diff --git a/.dockerignore b/.dockerignore index d6abd1451..0adca0b32 100644 --- a/.dockerignore +++ b/.dockerignore @@ -22,3 +22,6 @@ yarn-error.log /_data .rnd /.ssh +.ignition.json +.env.dusk.local +docker/coolify-realtime/node_modules diff --git a/.env.development.example b/.env.development.example index 920c32d92..d4daed4f7 100644 --- a/.env.development.example +++ b/.env.development.example @@ -1,16 +1,34 @@ -APP_NAME=Coolify-localhost -APP_ID=development +# Coolify Configuration APP_ENV=local +APP_NAME="Coolify Development" +APP_ID=development APP_KEY= -APP_DEBUG=true APP_URL=http://localhost APP_PORT=8000 -MUX_ENABLED=false +APP_DEBUG=true +SSH_MUX_ENABLED=true +# PostgreSQL Database Configuration +DB_DATABASE=coolify +DB_USERNAME=coolify +DB_PASSWORD=password +DB_HOST=host.docker.internal +DB_PORT=5432 + +# Ray Configuration +# Set to true to enable Ray +RAY_ENABLED=false +# Set custom ray port +# RAY_PORT= + +# Enable Laravel Telescope for debugging +TELESCOPE_ENABLED=false + +# Selenium Driver URL for Dusk DUSK_DRIVER_URL=http://selenium:4444 -## For Andras only -# To purge cache +# Special Keys for Andras +# For cache purging BUNNY_API_KEY= -# To upload assets +# For asset uploads BUNNY_STORAGE_API_KEY= diff --git a/.env.dusk.ci b/.env.dusk.ci new file mode 100644 index 000000000..9660de7b4 --- /dev/null +++ b/.env.dusk.ci @@ -0,0 +1,15 @@ +APP_ENV=production +APP_NAME="Coolify Staging" +APP_ID=development +APP_KEY= +APP_URL=http://localhost +APP_PORT=8000 +SSH_MUX_ENABLED=true + +# PostgreSQL Database Configuration +DB_DATABASE=coolify +DB_USERNAME=coolify +DB_PASSWORD=password +DB_HOST=localhost +DB_PORT=5432 + diff --git a/.env.production b/.env.production index f15a8b0e9..099ec7c25 100644 --- a/.env.production +++ b/.env.production @@ -1,10 +1,16 @@ +# Coolify Configuration APP_ID= APP_NAME=Coolify APP_KEY= +# PostgreSQL Database Configuration +DB_USERNAME=coolify DB_PASSWORD= + +# Redis Configuration REDIS_PASSWORD= +# Pusher Configuration PUSHER_APP_ID= PUSHER_APP_KEY= PUSHER_APP_SECRET= diff --git a/.env.windows-docker-desktop.example b/.env.windows-docker-desktop.example index 02a5a4174..b067b4c5c 100644 --- a/.env.windows-docker-desktop.example +++ b/.env.windows-docker-desktop.example @@ -4,6 +4,7 @@ APP_ID=coolify-windows-docker-desktop APP_NAME=Coolify APP_KEY=base64:ssTlCmrIE/q7whnKMvT6DwURikg69COzGsAwFVROm80= +DB_USERNAME=coolify DB_PASSWORD=coolify REDIS_PASSWORD=coolify diff --git a/.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml new file mode 100644 index 000000000..42df4785e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml @@ -0,0 +1,65 @@ +name: 🐞 Bug Report +description: "File a new bug report." +title: "[Bug]: " +labels: ["🐛 Bug", "🔍 Triage"] +body: + - type: markdown + attributes: + value: | + > [!IMPORTANT] + > **Please ensure you are using the latest version of Coolify before submitting an issue, as the bug may have already been fixed in a recent update.** (Of course, if you're experiencing an issue on the latest version that wasn't present in a previous version, please let us know.) + + # 💎 Bounty Program (with [algora.io](https://console.algora.io/org/coollabsio/bounties/new)) + - If you would like to prioritize the issue resolution, consider adding a bounty to this issue through our [Bounty Program](https://console.algora.io/org/coollabsio/bounties/new). + + - type: textarea + attributes: + label: Error Message and Logs + description: Provide a detailed description of the error or exception you encountered, along with any relevant log output. + validations: + required: true + + - type: textarea + attributes: + label: Steps to Reproduce + description: Please provide a step-by-step guide to reproduce the issue. Be as detailed as possible, otherwise we may not be able to assist you. + value: | + 1. + 2. + 3. + 4. + validations: + required: true + + - type: input + attributes: + label: Example Repository URL + description: If applicable, provide a URL to a repository demonstrating the issue. + + - type: input + attributes: + label: Coolify Version + description: Please provide the Coolify version you are using. This can be found in the top left corner of your Coolify dashboard. + placeholder: "v4.0.0-beta.335" + validations: + required: true + + - type: dropdown + attributes: + label: Are you using Coolify Cloud? + options: + - "No (self-hosted)" + - "Yes (Coolify Cloud)" + validations: + required: true + + - type: input + attributes: + label: Operating System and Version (self-hosted) + description: Run `cat /etc/os-release` or `lsb_release -a` in your terminal and provide the operating system and version. + placeholder: "Ubuntu 22.04" + + - type: textarea + attributes: + label: Additional Information + description: Any other relevant details about the issue. diff --git a/.github/ISSUE_TEMPLATE/02_ENHANCEMENT_BOUNTY.yml b/.github/ISSUE_TEMPLATE/02_ENHANCEMENT_BOUNTY.yml new file mode 100644 index 000000000..ef26125e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_ENHANCEMENT_BOUNTY.yml @@ -0,0 +1,31 @@ +name: 💎 Enhancement Bounty +description: "Propose a new feature, service, or improvement with an attached bounty." +title: "[Enhancement]: " +labels: ["✨ Enhancement", "🔍 Triage"] +body: + - type: markdown + attributes: + value: | + > [!IMPORTANT] + > **This issue template is exclusively for proposing new features, services, or improvements with an attached bounty.** Enhancements without a bounty can be discussed in the appropriate category of [Github Discussions](https://github.com/coollabsio/coolify/discussions). + + # 💎 Add a Bounty (with [algora.io](https://console.algora.io/org/coollabsio/bounties/new)) + - [Click here to add the required bounty](https://console.algora.io/org/coollabsio/bounties/new) + + - type: dropdown + attributes: + label: Request Type + description: Select the type of request you are making. + options: + - New Feature + - New Service + - Improvement + validations: + required: true + + - type: textarea + attributes: + label: Description + description: Provide a detailed description of the feature, improvement, or service you are proposing. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml deleted file mode 100644 index 5ee5c3970..000000000 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Bug report -description: 'Create a new bug report.' -title: '[Bug]: ' -body: - - type: markdown - attributes: - value: >- - # 💎 Bounty program (with - [algora.io](https://console.algora.io/org/coollabsio/bounties/new)) - - - If you would like to prioritize the issue resolution, you can add bounty - to this issue. - - - Click [here](https://console.algora.io/org/coollabsio/bounties/new) to - get started. - - type: textarea - attributes: - label: Description - description: A clear and concise description of the problem - - type: textarea - attributes: - label: Minimal Reproduction (if possible, example repository) - description: Please provide a step by step guide to reproduce the issue. - validations: - required: true - - type: textarea - attributes: - label: Exception or Error - description: Please provide error logs if possible. - - type: input - attributes: - label: Version - description: Coolify's version (see top of your screen). - validations: - required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 2d354057e..92c48e2d6 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,18 @@ blank_issues_enabled: false + contact_links: - - name: 🤔 Community Support (Chat) + - name: 🤔 Questions and Community Support url: https://coollabs.io/discord - about: Reach out to us on Discord. - - name: 🙋‍♂️ Feature Requests - url: https://github.com/coollabsio/coolify/discussions/categories/feature-requests-ideas - about: All feature requests will be discussed here. + about: If you have any questions, reach out to us on Discord inside the "#support" channel. + + - name: 💡 Feature Request + url: https://github.com/coollabsio/coolify/discussions/categories/feature-requests + about: Suggest a new feature for Coolify. + + - name: ⚙️ Service Request + url: https://github.com/coollabsio/coolify/discussions/categories/service-requests + about: Request a new service integration for Coolify. + + - name: 🔧 Improvements + url: https://github.com/coollabsio/coolify/discussions/categories/improvements + about: Suggest improvements to existing features for Coolify. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 3ded74ce3..5afe00a30 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1 +1,13 @@ -> Always use `next` branch as destination branch for PRs, not `main` +## Submit Checklist (REMOVE THIS SECTION BEFORE SUBMITTING) +- [ ] I have selected the `next` branch as the destination for my PR, not `main`. +- [ ] I have listed all changes in the `Changes` section. +- [ ] I have filled out the `Issues` section with the issue/discussion link(s) (if applicable). +- [ ] I have tested my changes. +- [ ] I have considered backwards compatibility. +- [ ] I have removed this checklist and any unused sections. + +## Changes +- + +## Issues +- fix # diff --git a/.github/workflows/browser-tests.yml b/.github/workflows/browser-tests.yml new file mode 100644 index 000000000..b06c9e97c --- /dev/null +++ b/.github/workflows/browser-tests.yml @@ -0,0 +1,65 @@ +name: Dusk +on: + push: + branches: [ "not-existing" ] +jobs: + dusk: + runs-on: ubuntu-latest + + services: + redis: + image: redis + env: + REDIS_HOST: localhost + REDIS_PORT: 6379 + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v4 + - name: Set up PostgreSQL + run: | + sudo systemctl start postgresql + sudo -u postgres psql -c "CREATE DATABASE coolify;" + sudo -u postgres psql -c "CREATE USER coolify WITH PASSWORD 'password';" + sudo -u postgres psql -c "ALTER ROLE coolify SET client_encoding TO 'utf8';" + sudo -u postgres psql -c "ALTER ROLE coolify SET default_transaction_isolation TO 'read committed';" + sudo -u postgres psql -c "ALTER ROLE coolify SET timezone TO 'UTC';" + sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE coolify TO coolify;" + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + - name: Copy .env + run: cp .env.dusk.ci .env + - name: Install Dependencies + run: composer install --no-progress --prefer-dist --optimize-autoloader + - name: Generate key + run: php artisan key:generate + - name: Install Chrome binaries + run: php artisan dusk:chrome-driver --detect + - name: Start Chrome Driver + run: ./vendor/laravel/dusk/bin/chromedriver-linux --port=4444 & + - name: Build assets + run: npm install && npm run build + - name: Run Laravel Server + run: php artisan serve --no-reload & + - name: Execute tests + run: php artisan dusk + - name: Upload Screenshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: screenshots + path: tests/Browser/screenshots + - name: Upload Console Logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: console + path: tests/Browser/console diff --git a/.github/workflows/chore-lock-closed-issues-discussions-and-prs.yml b/.github/workflows/chore-lock-closed-issues-discussions-and-prs.yml new file mode 100644 index 000000000..d00853964 --- /dev/null +++ b/.github/workflows/chore-lock-closed-issues-discussions-and-prs.yml @@ -0,0 +1,17 @@ +name: Lock closed Issues, Discussions, and PRs + +on: + schedule: + - cron: '0 1 * * *' + +jobs: + lock-threads: + runs-on: ubuntu-latest + steps: + - name: Lock threads after 30 days of inactivity + uses: dessant/lock-threads@v5 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + issue-inactive-days: '30' + pr-inactive-days: '30' + discussion-inactive-days: '30' diff --git a/.github/workflows/chore-manage-stale-issues-and-prs.yml b/.github/workflows/chore-manage-stale-issues-and-prs.yml new file mode 100644 index 000000000..2afc996cb --- /dev/null +++ b/.github/workflows/chore-manage-stale-issues-and-prs.yml @@ -0,0 +1,28 @@ +name: Manage Stale Issues and PRs + +on: + schedule: + - cron: '0 2 * * *' + +jobs: + manage-stale: + runs-on: ubuntu-latest + steps: + - name: Manage stale issues and PRs + uses: actions/stale@v9 + id: stale + with: + stale-issue-message: 'This issue will be automatically closed in a few days if no response is received. Please provide an update with the requested information.' + stale-pr-message: 'This pull request will be automatically closed in a few days if no response is received. Please update your PR or comment if you would like to continue working on it.' + close-issue-message: 'This issue has been automatically closed due to inactivity.' + close-pr-message: 'This pull request has been automatically closed due to inactivity.' + days-before-stale: 14 + days-before-close: 7 + stale-issue-label: '⏱︎ Stale' + stale-pr-label: '⏱︎ Stale' + only-labels: '💤 Waiting for feedback' + remove-stale-when-updated: true + operations-per-run: 100 + labels-to-remove-when-unstale: '⏱︎ Stale, 💤 Waiting for feedback' + close-issue-reason: 'not_planned' + exempt-all-milestones: false diff --git a/.github/workflows/chore-remove-labels-and-assignees-on-close.yml b/.github/workflows/chore-remove-labels-and-assignees-on-close.yml new file mode 100644 index 000000000..ea097e328 --- /dev/null +++ b/.github/workflows/chore-remove-labels-and-assignees-on-close.yml @@ -0,0 +1,78 @@ +name: Remove Labels and Assignees on Issue Close + +on: + issues: + types: [closed] + pull_request: + types: [closed] + pull_request_target: + types: [closed] + +jobs: + remove-labels-and-assignees: + runs-on: ubuntu-latest + steps: + - name: Remove labels and assignees + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { owner, repo } = context.repo; + + async function processIssue(issueNumber) { + try { + const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ + owner, + repo, + issue_number: issueNumber + }); + + const labelsToKeep = currentLabels + .filter(label => label.name === '⏱︎ Stale') + .map(label => label.name); + + await github.rest.issues.setLabels({ + owner, + repo, + issue_number: issueNumber, + labels: labelsToKeep + }); + + const { data: issue } = await github.rest.issues.get({ + owner, + repo, + issue_number: issueNumber + }); + + if (issue.assignees && issue.assignees.length > 0) { + await github.rest.issues.removeAssignees({ + owner, + repo, + issue_number: issueNumber, + assignees: issue.assignees.map(assignee => assignee.login) + }); + } + } catch (error) { + if (error.status !== 404) { + console.error(`Error processing issue ${issueNumber}:`, error); + } + } + } + + if (context.eventName === 'issues' || context.eventName === 'pull_request' || context.eventName === 'pull_request_target') { + const issue = context.payload.issue || context.payload.pull_request; + await processIssue(issue.number); + } + + if (context.eventName === 'pull_request' || context.eventName === 'pull_request_target') { + const pr = context.payload.pull_request; + if (pr.body) { + const issueReferences = pr.body.match(/#(\d+)/g); + if (issueReferences) { + for (const reference of issueReferences) { + const issueNumber = parseInt(reference.substring(1)); + await processIssue(issueNumber); + } + } + } + } diff --git a/.github/workflows/coolify-helper-next.yml b/.github/workflows/coolify-helper-next.yml index d9921b363..4354294b1 100644 --- a/.github/workflows/coolify-helper-next.yml +++ b/.github/workflows/coolify-helper-next.yml @@ -1,4 +1,4 @@ -name: Coolify Helper Image Development (v4) +name: Coolify Helper Image Development on: push: @@ -8,7 +8,8 @@ on: - docker/coolify-helper/Dockerfile env: - REGISTRY: ghcr.io + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io IMAGE_NAME: "coollabsio/coolify-helper" jobs: @@ -19,21 +20,38 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/coolify-helper/Dockerfile platforms: linux/amd64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next + labels: | + coolify.managed=true aarch64: runs-on: [ self-hosted, arm64 ] permissions: @@ -41,21 +59,39 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/coolify-helper/Dockerfile platforms: linux/aarch64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64 + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 + labels: | + coolify.managed=true + merge-manifest: runs-on: ubuntu-latest permissions: @@ -63,22 +99,44 @@ jobs: packages: write needs: [ amd64, aarch64 ] steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Create & publish manifest + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next + - uses: sarisia/actions-status-discord@v1 if: always() with: webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} + diff --git a/.github/workflows/coolify-helper.yml b/.github/workflows/coolify-helper.yml index 7e8132ec6..6d852a2b3 100644 --- a/.github/workflows/coolify-helper.yml +++ b/.github/workflows/coolify-helper.yml @@ -1,4 +1,4 @@ -name: Coolify Helper Image (v4) +name: Coolify Helper Image on: push: @@ -8,7 +8,8 @@ on: - docker/coolify-helper/Dockerfile env: - REGISTRY: ghcr.io + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io IMAGE_NAME: "coollabsio/coolify-helper" jobs: @@ -19,21 +20,38 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/coolify-helper/Dockerfile platforms: linux/amd64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + labels: | + coolify.managed=true aarch64: runs-on: [ self-hosted, arm64 ] permissions: @@ -41,21 +59,38 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/coolify-helper/Dockerfile platforms: linux/aarch64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + labels: | + coolify.managed=true merge-manifest: runs-on: ubuntu-latest permissions: @@ -63,22 +98,45 @@ jobs: packages: write needs: [ amd64, aarch64 ] steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Create & publish manifest + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + - uses: sarisia/actions-status-discord@v1 if: always() with: webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }} + diff --git a/.github/workflows/coolify-production-build.yml b/.github/workflows/coolify-production-build.yml new file mode 100644 index 000000000..79dfc3fc0 --- /dev/null +++ b/.github/workflows/coolify-production-build.yml @@ -0,0 +1,139 @@ +name: Production Build (v4) + +on: + push: + branches: ["main"] + paths-ignore: + - .github/workflows/coolify-helper.yml + - .github/workflows/coolify-helper-next.yml + - .github/workflows/coolify-realtime.yml + - .github/workflows/coolify-realtime-next.yml + - docker/coolify-helper/Dockerfile + - docker/coolify-realtime/Dockerfile + - docker/testing-host/Dockerfile + - templates/* + +env: + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io + IMAGE_NAME: "coollabsio/coolify" + +jobs: + amd64: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT + + - name: Build and Push Image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/prod/Dockerfile + platforms: linux/amd64 + push: true + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + + aarch64: + runs-on: [self-hosted, arm64] + steps: + - uses: actions/checkout@v4 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT + + - name: Build and Push Image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/prod/Dockerfile + platforms: linux/aarch64 + push: true + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + + merge-manifest: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + needs: [amd64, aarch64] + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - uses: sarisia/actions-status-discord@v1 + if: always() + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }} diff --git a/.github/workflows/coolify-realtime-next.yml b/.github/workflows/coolify-realtime-next.yml new file mode 100644 index 000000000..ef247170f --- /dev/null +++ b/.github/workflows/coolify-realtime-next.yml @@ -0,0 +1,147 @@ +name: Coolify Realtime Development + +on: + push: + branches: [ "next" ] + paths: + - .github/workflows/coolify-realtime-next.yml + - docker/coolify-realtime/Dockerfile + - docker/coolify-realtime/terminal-server.js + - docker/coolify-realtime/package.json + - docker/coolify-realtime/package-lock.json + - docker/coolify-realtime/soketi-entrypoint.sh + +env: + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io + IMAGE_NAME: "coollabsio/coolify-realtime" + +jobs: + amd64: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT + + - name: Build and Push Image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/coolify-realtime/Dockerfile + platforms: linux/amd64 + push: true + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next + labels: | + coolify.managed=true + + aarch64: + runs-on: [ self-hosted, arm64 ] + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT + + - name: Build and Push Image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/coolify-realtime/Dockerfile + platforms: linux/aarch64 + push: true + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 + labels: | + coolify.managed=true + + merge-manifest: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + needs: [ amd64, aarch64 ] + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next + + - uses: sarisia/actions-status-discord@v1 + if: always() + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} diff --git a/.github/workflows/coolify-realtime.yml b/.github/workflows/coolify-realtime.yml new file mode 100644 index 000000000..9654a21b0 --- /dev/null +++ b/.github/workflows/coolify-realtime.yml @@ -0,0 +1,147 @@ +name: Coolify Realtime + +on: + push: + branches: [ "main" ] + paths: + - .github/workflows/coolify-realtime.yml + - docker/coolify-realtime/Dockerfile + - docker/coolify-realtime/terminal-server.js + - docker/coolify-realtime/package.json + - docker/coolify-realtime/package-lock.json + - docker/coolify-realtime/soketi-entrypoint.sh + +env: + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io + IMAGE_NAME: "coollabsio/coolify-realtime" + +jobs: + amd64: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT + + - name: Build and Push Image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/coolify-realtime/Dockerfile + platforms: linux/amd64 + push: true + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + labels: | + coolify.managed=true + + aarch64: + runs-on: [ self-hosted, arm64 ] + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT + + - name: Build and Push Image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/coolify-realtime/Dockerfile + platforms: linux/aarch64 + push: true + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + labels: | + coolify.managed=true + + merge-manifest: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + needs: [ amd64, aarch64 ] + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - uses: sarisia/actions-status-discord@v1 + if: always() + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }} diff --git a/.github/workflows/coolify-staging-build.yml b/.github/workflows/coolify-staging-build.yml new file mode 100644 index 000000000..1aafc2f0b --- /dev/null +++ b/.github/workflows/coolify-staging-build.yml @@ -0,0 +1,125 @@ +name: Staging Build + +on: + push: + branches-ignore: ["main", "v3"] + paths-ignore: + - .github/workflows/coolify-helper.yml + - .github/workflows/coolify-helper-next.yml + - .github/workflows/coolify-realtime.yml + - .github/workflows/coolify-realtime-next.yml + - docker/coolify-helper/Dockerfile + - docker/coolify-realtime/Dockerfile + - docker/testing-host/Dockerfile + - templates/* + +env: + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io + IMAGE_NAME: "coollabsio/coolify" + +jobs: + amd64: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and Push Image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/prod/Dockerfile + platforms: linux/amd64 + push: true + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + + aarch64: + runs-on: [self-hosted, arm64] + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and Push Image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/prod/Dockerfile + platforms: linux/aarch64 + push: true + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 + + merge-manifest: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + needs: [amd64, aarch64] + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + + - uses: sarisia/actions-status-discord@v1 + if: always() + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} diff --git a/.github/workflows/coolify-testing-host.yml b/.github/workflows/coolify-testing-host.yml index 5fdc32991..95a228114 100644 --- a/.github/workflows/coolify-testing-host.yml +++ b/.github/workflows/coolify-testing-host.yml @@ -1,14 +1,15 @@ -name: Coolify Testing Host (v4-non-prod) +name: Coolify Testing Host on: push: - branches: [ "main", "next" ] + branches: [ "next" ] paths: - .github/workflows/coolify-testing-host.yml - docker/testing-host/Dockerfile env: - REGISTRY: ghcr.io + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io IMAGE_NAME: "coollabsio/coolify-testing-host" jobs: @@ -19,21 +20,34 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/testing-host/Dockerfile platforms: linux/amd64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + labels: | + coolify.managed=true + aarch64: runs-on: [ self-hosted, arm64 ] permissions: @@ -41,21 +55,34 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/testing-host/Dockerfile platforms: linux/aarch64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 + labels: | + coolify.managed=true + merge-manifest: runs-on: ubuntu-latest permissions: @@ -63,21 +90,36 @@ jobs: packages: write needs: [ amd64, aarch64 ] steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Create & publish manifest + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + - uses: sarisia/actions-status-discord@v1 if: always() with: diff --git a/.github/workflows/development-build.yml b/.github/workflows/development-build.yml deleted file mode 100644 index 268b885ac..000000000 --- a/.github/workflows/development-build.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: Development Build (v4) - -on: - push: - branches-ignore: ["main", "v3"] - paths-ignore: - - .github/workflows/coolify-helper.yml - - docker/coolify-helper/Dockerfile - -env: - REGISTRY: ghcr.io - IMAGE_NAME: "coollabsio/coolify" - -jobs: - amd64: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 - with: - context: . - file: docker/prod/Dockerfile - platforms: linux/amd64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} - aarch64: - runs-on: [self-hosted, arm64] - permissions: - contents: read - packages: write - steps: - - uses: actions/checkout@v4 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 - with: - context: . - file: docker/prod/Dockerfile - platforms: linux/aarch64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 - merge-manifest: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - needs: [amd64, aarch64] - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Create & publish manifest - run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} - - uses: sarisia/actions-status-discord@v1 - if: always() - with: - webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml deleted file mode 100644 index 0edaa4f1c..000000000 --- a/.github/workflows/docker-image.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Docker Image CI - -on: - # push: - # branches: [ "main" ] - # pull_request: - # branches: [ "*" ] - push: - branches: ["this-does-not-exist"] - pull_request: - branches: ["this-does-not-exist"] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Cache Docker layers - uses: actions/cache@v2 - with: - path: | - /usr/local/share/ca-certificates - /var/cache/apt/archives - /var/lib/apt/lists - ~/.cache - key: ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }} - restore-keys: | - ${{ runner.os }}-docker- - - name: Build the Docker image - run: | - cp .env.example .env - docker run --rm -u "$(id -u):$(id -g)" \ - -v "$(pwd):/app" \ - -w /app composer:2 \ - composer install --ignore-platform-reqs - ./vendor/bin/spin build - - name: Start the stack - run: | - ./vendor/bin/spin up -d - ./vendor/bin/spin exec coolify php artisan key:generate - ./vendor/bin/spin exec coolify php artisan migrate:fresh --seed - - name: Test (missing E2E tests) - run: | - ./vendor/bin/spin exec coolify php artisan test diff --git a/.github/workflows/fix-php-code-style-issues.yml b/.github/workflows/fix-php-code-style-issues.yml deleted file mode 100644 index aebce91bc..000000000 --- a/.github/workflows/fix-php-code-style-issues.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Fix PHP code style issues - -on: [push] - -permissions: - contents: write - -jobs: - php-code-styling: - runs-on: ubuntu-latest - timeout-minutes: 5 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - - - name: Fix PHP code style issues - uses: aglipanci/laravel-pint-action@2.4 - - - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: Fix styling diff --git a/.github/workflows/production-build.yml b/.github/workflows/production-build.yml deleted file mode 100644 index e4bad6a65..000000000 --- a/.github/workflows/production-build.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: Production Build (v4) - -on: - push: - branches: ["main"] - paths-ignore: - - templates/service-templates.json - -env: - REGISTRY: ghcr.io - IMAGE_NAME: "coollabsio/coolify" - -jobs: - amd64: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Get Version - id: version - run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 - with: - context: . - file: docker/prod/Dockerfile - platforms: linux/amd64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} - aarch64: - runs-on: [self-hosted, arm64] - steps: - - uses: actions/checkout@v4 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Get Version - id: version - run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 - with: - context: . - file: docker/prod/Dockerfile - platforms: linux/aarch64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 - merge-manifest: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - needs: [amd64, aarch64] - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Get Version - id: version - run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT - - name: Create & publish manifest - run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest - - uses: sarisia/actions-status-discord@v1 - if: always() - with: - webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }} diff --git a/.gitignore b/.gitignore index ac8a1e090..dd6b141b9 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ _ide_helper_models.php .rnd /.ssh scripts/load-test/* +.ignition.json +.env.dusk.local +docker/coolify-realtime/node_modules diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..80ec0614e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,243 @@ +# Contributing to Coolify + +> "First, thanks for considering contributing to my project. It really means a lot!" - [@andrasbacsai](https://github.com/andrasbacsai) + +You can ask for guidance anytime on our [Discord server](https://coollabs.io/discord) in the `#contribute` channel. + +## Table of Contents + +1. [Setup Development Environment](#1-setup-development-environment) +2. [Verify Installation](#2-verify-installation-optional) +3. [Fork and Setup Local Repository](#3-fork-and-setup-local-repository) +4. [Set up Environment Variables](#4-set-up-environment-variables) +5. [Start Coolify](#5-start-coolify) +6. [Start Development](#6-start-development) +7. [Create a Pull Request](#7-create-a-pull-request) +8. [Development Notes](#development-notes) +9. [Resetting Development Environment](#resetting-development-environment) +10. [Additional Contribution Guidelines](#additional-contribution-guidelines) + +## 1. Setup Development Environment + +Follow the steps below for your operating system: + +
+Windows + +1. Install `docker-ce`, Docker Desktop (or similar): + - Docker CE (recommended): + - Install Windows Subsystem for Linux v2 (WSL2) by following this guide: [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install?ref=coolify) + - After installing WSL2, install Docker CE for your Linux distribution by following this guide: [Install Docker Engine](https://docs.docker.com/engine/install/?ref=coolify) + - Make sure to choose the appropriate Linux distribution (e.g., Ubuntu) when following the Docker installation guide + - Install Docker Desktop (easier): + - Download and install [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/?ref=coolify) + - Ensure WSL2 backend is enabled in Docker Desktop settings + +2. Install Spin: + - Follow the instructions to install Spin on Windows from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-windows#download-and-install-spin-into-wsl2?ref=coolify) + +
+ +
+MacOS + +1. Install Orbstack, Docker Desktop (or similar): + - Orbstack (recommended, as it is a faster and lighter alternative to Docker Desktop): + - Download and install [Orbstack](https://docs.orbstack.dev/quick-start#installation?ref=coolify) + - Docker Desktop: + - Download and install [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/?ref=coolify) + +2. Install Spin: + - Follow the instructions to install Spin on MacOS from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-macos/#download-and-install-spin?ref=coolify) + +
+ +
+Linux + +1. Install Docker Engine, Docker Desktop (or similar): + - Docker Engine (recommended, as there is no VM overhead): + - Follow the official [Docker Engine installation guide](https://docs.docker.com/engine/install/?ref=coolify) for your Linux distribution + - Docker Desktop: + - If you want a GUI, you can use [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/?ref=coolify) + +2. Install Spin: + - Follow the instructions to install Spin on Linux from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-linux#configure-docker-permissions?ref=coolify) + +
+ +## 2. Verify Installation (Optional) + +After installing Docker (or Orbstack) and Spin, verify the installation: + +1. Open a terminal or command prompt +2. Run the following commands: + ```bash + docker --version + spin --version + ``` + You should see version information for both Docker and Spin. + +## 3. Fork and Setup Local Repository + +1. Fork the [Coolify](https://github.com/coollabsio/coolify) repository to your GitHub account. + +2. Install a code editor on your machine (choose one): + + | Editor | Platform | Download Link | + |--------|----------|---------------| + | Visual Studio Code (recommended free) | Windows/macOS/Linux | [Download](https://code.visualstudio.com/download?ref=coolify) | + | Cursor (recommended but paid) | Windows/macOS/Linux | [Download](https://www.cursor.com/?ref=coolify) | + | Zed (very fast) | macOS/Linux | [Download](https://zed.dev/download?ref=coolify) | + +3. Clone the Coolify Repository from your fork to your local machine + - Use `git clone` in the command line, or + - Use GitHub Desktop (recommended): + - Download and install from [https://desktop.github.com/](https://desktop.github.com/?ref=coolify) + - Open GitHub Desktop and login with your GitHub account + - Click on `File` -> `Clone Repository` select `github.com` as the repository location, then select your forked Coolify repository, choose the local path and then click `Clone` + +4. Open the cloned Coolify Repository in your chosen code editor. + +## 4. Set up Environment Variables + +1. In the Code Editor, locate the `.env.development.example` file in the root directory of your local Coolify repository. +2. Duplicate the `.env.development.example` file and rename the copy to `.env`. +3. Open the new `.env` file and review its contents. Adjust any environment variables as needed for your development setup. +4. If you encounter errors during database migrations, update the database connection settings in your `.env` file. Use the IP address or hostname of your PostgreSQL database container. You can find this information by running `docker ps` after executing `spin up`. +5. Save the changes to your `.env` file. + +## 5. Start Coolify + +1. Open a terminal in the local Coolify directory. +2. Run the following command in the terminal (leave that terminal open): + ```bash + spin up + ``` + +> [!NOTE] +> You may see some errors, but don't worry; this is expected. + +3. If you encounter permission errors, especially on macOS, use: + ```bash + sudo spin up + ``` + +> [!NOTE] +> If you change environment variables afterwards or anything seems broken, press Ctrl + C to stop the process and run `spin up` again. + +## 6. Start Development + +1. Access your Coolify instance: + - URL: `http://localhost:8000` + - Login: `test@example.com` + - Password: `password` + +2. Additional development tools: + | Tool | URL | Note | + |------|-----|------| + | Laravel Horizon (scheduler) | `http://localhost:8000/horizon` | Only accessible when logged in as root user | + | Mailpit (email catcher) | `http://localhost:8025` | | + | Telescope (debugging tool) | `http://localhost:8000/telescope` | Disabled by default | + +> [!NOTE] +> To enable Telescope, add the following to your `.env` file: +> ```env +> TELESCOPE_ENABLED=true +> ``` + +## 7. Create a Pull Request + +1. After making changes or adding a new service: + - Commit your changes to your forked repository. + - Push the changes to your GitHub account. + +2. Creating the Pull Request (PR): + - Navigate to the main Coolify repository on GitHub. + - Click the "Pull requests" tab. + - Click the green "New pull request" button. + - Choose your fork and branch as the compare branch. + - Click "Create pull request". + +3. Filling out the PR details: + - Give your PR a descriptive title. + - Use the Pull Request Template provided and fill in the details. + +> [!IMPORTANT] +> Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `main` branch. + +4. Submit your PR: + - Review your changes one last time. + - Click "Create pull request" to submit. + +> [!NOTE] +> Make sure your PR is out of draft mode as soon as it's ready for review. PRs that are in draft mode for a long time may be closed by maintainers. + +After submission, maintainers will review your PR and may request changes or provide feedback. + +## Development Notes + +When working on Coolify, keep the following in mind: + +1. **Database Migrations**: After switching branches or making changes to the database structure, always run migrations: + ```bash + docker exec -it coolify php artisan migrate + ``` + +2. **Resetting Development Setup**: To reset your development setup to a clean database with default values: + ```bash + docker exec -it coolify php artisan migrate:fresh --seed + ``` + +3. **Troubleshooting**: If you encounter unexpected behavior, ensure your database is up-to-date with the latest migrations and if possible reset the development setup to eliminate any environment-specific issues. + +> [!IMPORTANT] +> Forgetting to migrate the database can cause problems, so make it a habit to run migrations after pulling changes or switching branches. + +## Resetting Development Environment + +If you encounter issues or break your database or something else, follow these steps to start from a clean slate (works since `v4.0.0-beta.342`): + +1. Stop all running containers `ctrl + c`. + +2. Remove all Coolify containers: + ```bash + docker rm coolify coolify-db coolify-redis coolify-realtime coolify-testing-host coolify-minio coolify-vite-1 coolify-mail + ``` + +3. Remove Coolify volumes (it is possible that the volumes have no `coolify` prefix on your machine, in that case remove the prefix from the command): + ```bash + docker volume rm coolify_dev_backups_data coolify_dev_postgres_data coolify_dev_redis_data coolify_dev_coolify_data coolify_dev_minio_data + ``` + +4. Remove unused images: + ```bash + docker image prune -a + ``` + +5. Start Coolify again: + ```bash + spin up + ``` + +6. Run database migrations and seeders: + ```bash + docker exec -it coolify php artisan migrate:fresh --seed + ``` + +After completing these steps, you'll have a fresh development setup. + +> [!IMPORTANT] +> Always run database migrations and seeders after switching branches or pulling updates to ensure your local database structure matches the current codebase and includes necessary seed data. + +## Additional Contribution Guidelines + +### Contributing a New Service + +To add a new service to Coolify, please refer to our documentation: +[Adding a New Service](https://coolify.io/docs/knowledge-base/contribute/service) + +### Contributing to Documentation + +To contribute to the Coolify documentation, please refer to this guide: +[Contributing to the Coolify Documentation](https://github.com/coollabsio/documentation-coolify/blob/main/CONTRIBUTING.md) diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md deleted file mode 100644 index 02a21573c..000000000 --- a/CONTRIBUTION.md +++ /dev/null @@ -1,34 +0,0 @@ -# Contributing - -> "First, thanks for considering to contribute to my project. - It really means a lot!" - [@andrasbacsai](https://github.com/andrasbacsai) - -You can ask for guidance anytime on our -[Discord server](https://coollabs.io/discord) in the `#contribution` channel. - -## Code Contribution - -### 1) Setup your development environment - -- You need to have Docker Engine (or equivalent) [installed](https://docs.docker.com/engine/install/) on your system. -- For better DX, install [Spin](https://serversideup.net/open-source/spin/). - -### 2) Set your environment variables - -- Copy [.env.development.example](./.env.development.example) to .env. - -## 3) Start & setup Coolify - -- Run `spin up` - You can notice that errors will be thrown. Don't worry. - - If you see weird permission errors, especially on Mac, run `sudo spin up` instead. - -### 4) Start development -You can login your Coolify instance at `localhost:8000` with `test@example.com` and `password`. - -Your horizon (Laravel scheduler): `localhost:8000/horizon` - Only reachable if you logged in with root user. - -Mails are caught by Mailpit: `localhost:8025` - -## New Service Contribution -Check out the docs [here](https://coolify.io/docs/knowledge-base/add-a-service). - diff --git a/README.md b/README.md index 56bee004e..14a741088 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@ +![Latest Release Version](https://img.shields.io/badge/dynamic/json?labelColor=grey&color=6366f1&label=Latest_released_version&url=https%3A%2F%2Fcdn.coollabs.io%2Fcoolify%2Fversions.json&query=coolify.v4.version&style=for-the-badge +) + [![Bounty Issues](https://img.shields.io/static/v1?labelColor=grey&color=6366f1&label=Algora&message=%F0%9F%92%8E+Bounty+issues&style=for-the-badge)](https://console.algora.io/org/coollabsio/bounties/new) -[![Open Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Fcoollabsio%2Fbounties%3Fstatus%3Dopen&style=for-the-badge)](https://console.algora.io/org/coollabsio/bounties?status=open) -[![Rewarded Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Fcoollabsio%2Fbounties%3Fstatus%3Dcompleted&style=for-the-badge)](https://console.algora.io/org/coollabsio/bounties?status=completed) + # About the Project Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc. -It helps you to manage your servers, applications, databases on your own hardware, all you need is SSH connection. You can manage VPS, Bare Metal, Raspberry PI's anything. +It helps you manage your servers, applications, and databases on your own hardware; you only need an SSH connection. You can manage VPS, Bare Metal, Raspberry PIs, and anything else. -Imagine if you could have the ease of a cloud but with your own servers. That is **Coolify**. +Imagine having the ease of a cloud but with your own servers. That is **Coolify**. -No vendor lock-in, which means that all the configuration for your applications/databases/etc are saved to your server. So if you decide to stop using Coolify (oh nooo), you could still manage your running resources. You just lose the automations and all the magic. 🪄️ +No vendor lock-in, which means that all the configurations for your applications/databases/etc are saved to your server. So, if you decide to stop using Coolify (oh nooo), you could still manage your running resources. You lose the automations and all the magic. 🪄️ -For more information, take a look at our landing page [here](https://coolify.io). +For more information, take a look at our landing page at [coolify.io](https://coolify.io). # Installation @@ -22,36 +24,56 @@ You can find the installation script source [here](./scripts/install.sh). # Support -Contact us [here](https://coolify.io/docs/contact). +Contact us at [coolify.io/docs/contact](https://coolify.io/docs/contact). # Donations -To stay completely free, open-source, no feature behind paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the future development of the project. +To stay completely free and open-source, with no feature behind the paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the project's future development. -https://coolify.io/sponsorships +[coolify.io/sponsorships](https://coolify.io/sponsorships) Thank you so much! Special thanks to our biggest sponsors! -cccareers logo -hetzner logo -logto logo -bc direct logo -quantcdn logo -arcjet logo +### Special Sponsors + +![image](https://github.com/user-attachments/assets/c95a07df-7c5a-4e77-a35a-81f25fcbece1) + +* [CCCareers](https://cccareers.org/) - A career development platform connecting coding bootcamp graduates with job opportunities in the tech industry. +* [Hetzner](http://htznr.li/CoolifyXHetzner) - A German web hosting company offering affordable dedicated servers, cloud services, and web hosting solutions. +* [Logto](https://logto.io/?ref=coolify) - An open-source authentication and authorization solution for building secure login systems and managing user identities. +* [BC Direct](https://bc.direct/?ref=coolify.io) - A digital marketing agency specializing in e-commerce solutions and online business growth strategies. +* [QuantCDN](https://www.quantcdn.io/?ref=coolify.io) - A content delivery network (CDN) optimizing website performance through global content distribution. +* [Arcjet](https://arcjet.com/?ref=coolify.io) - A cloud-based platform providing real-time protection against API abuse and bot attacks. +* [SupaGuide](https://supa.guide/?ref=coolify.io) - A comprehensive resource hub offering guides and tutorials for web development using Supabase. +* [Tigris](https://tigrisdata.com/?ref=coolify.io) - A fully managed serverless object storage service compatible with Amazon S3 API. Offers high performance, scalability, and built-in search capabilities for efficient data management. +* [Fractal Networks](https://fractalnetworks.co/?ref=coolify.io) - A decentralized network infrastructure company focusing on secure and private communication solutions. +* [Advin](https://coolify.ad.vin/?ref=coolify.io) - A digital advertising agency specializing in programmatic advertising and data-driven marketing strategies. +* [Treive](https://trieve.ai/?ref=coolify.io) - An AI-powered search and discovery platform for enhancing information retrieval in large datasets. +* [Blacksmith](https://blacksmith.sh/?ref=coolify.io) - A cloud-native platform for automating infrastructure provisioning and management across multiple cloud providers. +* [Latitude](https://latitude.sh/?ref=coolify.io) - A cloud computing platform offering bare metal servers and cloud instances for developers and businesses. +* [Brand Dev](https://brand.dev/?ref=coolify.io) - A web development agency specializing in creating custom digital experiences and brand identities. +* [Jobscollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - A job search platform connecting professionals with remote work opportunities across various industries. +* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - A web hosting provider offering affordable hosting solutions, domain registration, and website building tools. +* [Glueops](https://www.glueops.dev/?ref=coolify.io) - A DevOps consulting company providing infrastructure automation and cloud optimization services. +* [Ubicloud](https://ubicloud.com/?ref=coolify.io) - An open-source alternative to hyperscale cloud providers, offering high-performance cloud computing services. +* [Juxtdigital](https://juxtdigital.dev/?ref=coolify.io) - A digital agency offering web development, design, and digital marketing services for businesses. +* [Saasykit](https://saasykit.com/?ref=coolify.io) - A Laravel-based boilerplate providing essential components and features for building SaaS applications quickly. +* [Massivegrid](https://massivegrid.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes. ## Github Sponsors ($40+) -SerpAPI -typebot - +SerpAPI +typebot + -Lightspeed.run - FlintCompany -American Cloud -CryptoJobsList -Thompson Edolo -UXWizz +Lightspeed.run + FlintCompany +American Cloud +CryptoJobsList +Codext +Thompson Edolo +UXWizz Younes Barrad Automaze Corentin Clichy @@ -61,8 +83,11 @@ Special thanks to our biggest sponsors! NiftyCo Imre Ujlaki Ilias Ism +Breakcold Paweł Pierścionek Michael Mazurczak +Formbricks +Adith Suhas ## Organizations @@ -83,9 +108,9 @@ Special thanks to our biggest sponsors! # Cloud -If you do not want to self-host Coolify, there is a paid cloud version available: https://app.coolify.io +If you do not want to self-host Coolify, there is a paid cloud version available: [app.coolify.io](https://app.coolify.io) -For more information & pricing, take a look at our landing page [here](https://coolify.io). +For more information & pricing, take a look at our landing page [coolify.io](https://coolify.io). ## Why should I use the Cloud version? The recommended way to use Coolify is to have one server for Coolify and one (or more) for the resources you are deploying. A server is around 4-5$/month. @@ -109,7 +134,7 @@ By subscribing to the cloud version, you get the Coolify server for the same pri

-Coolify - An open-source & self-hostable Heroku, Netlify alternative | Product Hunt +Coolify - An open-source & self-hostable Heroku, Netlify alternative | Product Hunt coollabsio%2Fcoolify | Trendshift diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 000000000..d9f05f17d --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,130 @@ +# Coolify Release Guide + +This guide outlines the release process for Coolify, intended for developers and those interested in understanding how releases are managed and deployed. + +## Table of Contents +- [Release Process](#release-process) +- [Version Types](#version-types) + - [Stable](#stable) + - [Nightly](#nightly) + - [Beta](#beta) +- [Version Availability](#version-availability) + - [Self-Hosted](#self-hosted) + - [Cloud](#cloud) +- [Manually Update to Specific Versions](#manually-update-to-specific-versions) + +## Release Process + +1. **Development on `next` or Feature Branches** + - Improvements, fixes, and new features are developed on the `next` branch or separate feature branches. + +2. **Merging to `main`** + - Once ready, changes are merged from the `next` branch into the `main` branch. + +3. **Building the Release** + - After merging to `main`, GitHub Actions automatically builds release images for all architectures and pushes them to the GitHub Container Registry with the version tag and the `latest` tag. + +4. **Creating a GitHub Release** + - A new GitHub release is manually created with details of the changes made in the version. + +5. **Updating the CDN** + - To make a new version publicly available, the version information on the CDN needs to be updated: [https://cdn.coollabs.io/coolify/versions.json](https://cdn.coollabs.io/coolify/versions.json) + +> [!NOTE] +> The CDN update may not occur immediately after the GitHub release. It can take hours or even days due to additional testing, stability checks, or potential hotfixes. **The update becomes available only after the CDN is updated.** + +## Version Types + +
+ Stable (coming soon) + +- **Stable** + - The production version suitable for stable, production environments (generally recommended). + - **Update Frequency:** Every 2 to 4 weeks, with more frequent possible hotfixes. + - **Release Size:** Larger but less frequent releases. Multiple nightly versions are consolidated into a single stable release. + - **Versioning Scheme:** Follows semantic versioning (e.g., `v4.0.0`). + - **Installation Command:** + ```bash + curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash + ``` + +
+ +
+ Nightly + +- **Nightly** + - The latest development version, suitable for testing the latest changes and experimenting with new features. + - **Update Frequency:** Daily or bi-weekly updates. + - **Release Size:** Smaller, more frequent releases. + - **Versioning Scheme:** TO BE DETERMINED + - **Installation Command:** + ```bash + curl -fsSL https://cdn.coollabs.io/coolify-nightly/install.sh | bash -s next + ``` + +
+ +
+ Beta + +- **Beta** + - Test releases for the upcoming stable version. + - **Purpose:** Allows users to test and provide feedback on new features and changes before they become stable. + - **Update Frequency:** Available if we think beta testing is necessary. + - **Release Size:** Same size as stable release as it will become the next stabe release after some time. + - **Versioning Scheme:** Follows semantic versioning (e.g., `4.1.0-beta.1`). + - **Installation Command:** + ```bash + curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash + ``` + +
+ +> [!WARNING] +> Do not use nightly/beta builds in production as there is no guarantee of stability. + +## Version Availability + +When a new version is released and a new GitHub release is created, it doesn't immediately become available for your instance. Here's how version availability works for different instance types. + +### Self-Hosted + +- **Update Frequency:** More frequent updates, especially on the nightly release channel. +- **Update Availability:** New versions are available once the CDN has been updated. +- **Update Methods:** + 1. **Manual Update in Instance Settings:** + - Go to `Settings > Update Check Frequency` and click the `Check Manually` button. + - If an update is available, an upgrade button will appear on the sidebar. + 2. **Automatic Update:** + - If enabled, the instance will update automatically at the time set in the settings. + 3. **Re-run Installation Script:** + - Run the installation script again to upgrade to the latest version available on the CDN: + ```bash + curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash + ``` + +> [!IMPORTANT] +> If a new release is available on GitHub but your instance hasn't updated yet or no upgrade button is shown in the UI, the CDN might not have been updated yet. This intentional delay ensures stability and allows for hotfixes before official release. + +### Cloud + +- **Update Frequency:** Less frequent as it's a managed service. +- **Update Availability:** New versions are available once Andras has updated the cloud version manually. +- **Update Method:** + - Updates are managed by Andras, who ensures each cloud version is thoroughly tested and stable before releasing it. + +> [!IMPORTANT] +> The cloud version of Coolify may be several versions behind the latest GitHub releases even if the CDN is updated. This is intentional to ensure stability and reliability for cloud users and Andras will manully update the cloud version when the update is ready. + +## Manually Update to Specific Versions + +> [!CAUTION] +> Updating to unreleased versions is not recommended and may cause issues. Use at your own risk! + +To update your Coolify instance to a specific (unreleased) version, use the following command: + +```bash +curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash -s +``` +Replace `` with the version you want to update to (for example `4.0.0-beta.332`). diff --git a/app/Actions/Application/GenerateConfig.php b/app/Actions/Application/GenerateConfig.php new file mode 100644 index 000000000..991146b48 --- /dev/null +++ b/app/Actions/Application/GenerateConfig.php @@ -0,0 +1,16 @@ +generateConfig(is_json: $is_json); + } +} diff --git a/app/Actions/Application/IsHorizonQueueEmpty.php b/app/Actions/Application/IsHorizonQueueEmpty.php new file mode 100644 index 000000000..17966b8a0 --- /dev/null +++ b/app/Actions/Application/IsHorizonQueueEmpty.php @@ -0,0 +1,37 @@ +getRecent(); + if ($recent) { + $running = $recent->filter(function ($job) use ($hostname) { + $payload = json_decode($job->payload); + $tags = data_get($payload, 'tags'); + + return $job->status != 'completed' && + $job->status != 'failed' && + isset($tags) && + is_array($tags) && + in_array('server:'.$hostname, $tags); + }); + if ($running->count() > 0) { + echo 'false'; + + return false; + } + } + echo 'true'; + + return true; + } +} diff --git a/app/Actions/Application/LoadComposeFile.php b/app/Actions/Application/LoadComposeFile.php new file mode 100644 index 000000000..838b541e2 --- /dev/null +++ b/app/Actions/Application/LoadComposeFile.php @@ -0,0 +1,16 @@ +loadComposeFile(); + } +} diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php index 446659e5b..cab7e45f0 100644 --- a/app/Actions/Application/StopApplication.php +++ b/app/Actions/Application/StopApplication.php @@ -2,6 +2,7 @@ namespace App\Actions\Application; +use App\Actions\Server\CleanupDocker; use App\Models\Application; use Lorisleiva\Actions\Concerns\AsAction; @@ -9,35 +10,32 @@ class StopApplication { use AsAction; - public function handle(Application $application) + public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true) { - if ($application->destination->server->isSwarm()) { - instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server); - - return; - } - - $servers = collect([]); - $servers->push($application->destination->server); - $application->additional_servers->map(function ($server) use ($servers) { - $servers->push($server); - }); - foreach ($servers as $server) { + try { + $server = $application->destination->server; if (! $server->isFunctional()) { return 'Server is not functional'; } - $containers = getCurrentApplicationContainerStatus($server, $application->id, 0); - if ($containers->count() > 0) { - foreach ($containers as $container) { - $containerName = data_get($container, 'Names'); - if ($containerName) { - instant_remote_process( - ["docker rm -f {$containerName}"], - $server - ); - } - } + + if ($server->isSwarm()) { + instant_remote_process(["docker stack rm {$application->uuid}"], $server); + + return; } + + $containersToStop = $application->getContainersToStop($previewDeployments); + $application->stopContainers($containersToStop, $server); + + if ($application->build_pack === 'dockercompose') { + $application->delete_connected_networks($application->uuid); + } + + if ($dockerCleanup) { + CleanupDocker::dispatch($server, true); + } + } catch (\Exception $e) { + return $e->getMessage(); } } } diff --git a/app/Actions/Application/StopApplicationOneServer.php b/app/Actions/Application/StopApplicationOneServer.php index da8c700fe..b13b10efd 100644 --- a/app/Actions/Application/StopApplicationOneServer.php +++ b/app/Actions/Application/StopApplicationOneServer.php @@ -32,8 +32,6 @@ class StopApplicationOneServer } } } catch (\Exception $e) { - ray($e->getMessage()); - return $e->getMessage(); } } diff --git a/app/Actions/CoolifyTask/PrepareCoolifyTask.php b/app/Actions/CoolifyTask/PrepareCoolifyTask.php index d4cdf64e2..6676b7937 100644 --- a/app/Actions/CoolifyTask/PrepareCoolifyTask.php +++ b/app/Actions/CoolifyTask/PrepareCoolifyTask.php @@ -3,6 +3,7 @@ namespace App\Actions\CoolifyTask; use App\Data\CoolifyTaskArgs; +use App\Enums\ActivityTypes; use App\Jobs\CoolifyTask; use Spatie\Activitylog\Models\Activity; @@ -40,8 +41,17 @@ class PrepareCoolifyTask public function __invoke(): Activity { - $job = new CoolifyTask($this->activity, ignore_errors: $this->remoteProcessArgs->ignore_errors, call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish, call_event_data: $this->remoteProcessArgs->call_event_data); - dispatch($job); + $job = new CoolifyTask( + activity: $this->activity, + ignore_errors: $this->remoteProcessArgs->ignore_errors, + call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish, + call_event_data: $this->remoteProcessArgs->call_event_data, + ); + if ($this->remoteProcessArgs->type === ActivityTypes::COMMAND->value) { + dispatch($job)->onQueue('high'); + } else { + dispatch($job); + } $this->activity->refresh(); return $this->activity; diff --git a/app/Actions/CoolifyTask/RunRemoteProcess.php b/app/Actions/CoolifyTask/RunRemoteProcess.php index be986a76f..981b81378 100644 --- a/app/Actions/CoolifyTask/RunRemoteProcess.php +++ b/app/Actions/CoolifyTask/RunRemoteProcess.php @@ -4,10 +4,12 @@ namespace App\Actions\CoolifyTask; use App\Enums\ActivityTypes; use App\Enums\ProcessStatus; +use App\Helpers\SshMultiplexingHelper; use App\Jobs\ApplicationDeploymentJob; use App\Models\Server; use Illuminate\Process\ProcessResult; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Process; use Spatie\Activitylog\Models\Activity; @@ -38,8 +40,7 @@ class RunRemoteProcess */ public function __construct(Activity $activity, bool $hide_from_output = false, bool $ignore_errors = false, $call_event_on_finish = null, $call_event_data = null) { - - if ($activity->getExtraProperty('type') !== ActivityTypes::INLINE->value) { + if ($activity->getExtraProperty('type') !== ActivityTypes::INLINE->value && $activity->getExtraProperty('type') !== ActivityTypes::COMMAND->value) { throw new \RuntimeException('Incompatible Activity to run a remote command.'); } @@ -124,7 +125,7 @@ class RunRemoteProcess ])); } } catch (\Throwable $e) { - ray($e); + Log::error('Error calling event: '.$e->getMessage()); } } @@ -137,7 +138,7 @@ class RunRemoteProcess $command = $this->activity->getExtraProperty('command'); $server = Server::whereUuid($server_uuid)->firstOrFail(); - return generateSshCommand($server, $command); + return SshMultiplexingHelper::generateSshCommand($server, $command); } protected function handleOutput(string $type, string $output) diff --git a/app/Actions/Database/RestartDatabase.php b/app/Actions/Database/RestartDatabase.php new file mode 100644 index 000000000..0400d924d --- /dev/null +++ b/app/Actions/Database/RestartDatabase.php @@ -0,0 +1,29 @@ +destination->server; + if (! $server->isFunctional()) { + return 'Server is not functional'; + } + StopDatabase::run($database); + + return StartDatabase::run($database); + } +} diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php index d9518cd80..13667e829 100644 --- a/app/Actions/Database/StartClickhouse.php +++ b/app/Actions/Database/StartClickhouse.php @@ -3,7 +3,6 @@ namespace App\Actions\Database; use App\Models\StandaloneClickhouse; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; @@ -52,6 +51,8 @@ class StartClickhouse ], 'labels' => [ 'coolify.managed' => 'true', + 'coolify.type' => 'database', + 'coolify.databaseId' => $this->database->id, ], 'healthcheck' => [ 'test' => "clickhouse-client --password {$this->database->clickhouse_admin_password} --query 'SELECT 1'", @@ -80,14 +81,7 @@ class StartClickhouse data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { - $docker_compose['services'][$container_name]['logging'] = [ - 'driver' => 'fluentd', - 'options' => [ - 'fluentd-address' => 'tcp://127.0.0.1:24224', - 'fluentd-async' => 'true', - 'fluentd-sub-second-precision' => 'true', - ], - ]; + $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration(); } if (count($this->database->ports_mappings_array) > 0) { $docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array; @@ -103,6 +97,11 @@ class StartClickhouse if (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } + + // Add custom docker run options + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; @@ -155,14 +154,16 @@ class StartClickhouse $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('CLICKHOUSE_ADMIN_USER'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_ADMIN_USER'))->isEmpty()) { $environment_variables->push("CLICKHOUSE_ADMIN_USER={$this->database->clickhouse_admin_user}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('CLICKHOUSE_ADMIN_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_ADMIN_PASSWORD'))->isEmpty()) { $environment_variables->push("CLICKHOUSE_ADMIN_PASSWORD={$this->database->clickhouse_admin_password}"); } + add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables); + return $environment_variables->all(); } } diff --git a/app/Actions/Database/StartDatabase.php b/app/Actions/Database/StartDatabase.php new file mode 100644 index 000000000..869a88521 --- /dev/null +++ b/app/Actions/Database/StartDatabase.php @@ -0,0 +1,57 @@ +destination->server; + if (! $server->isFunctional()) { + return 'Server is not functional'; + } + switch ($database->getMorphClass()) { + case \App\Models\StandalonePostgresql::class: + $activity = StartPostgresql::run($database); + break; + case \App\Models\StandaloneRedis::class: + $activity = StartRedis::run($database); + break; + case \App\Models\StandaloneMongodb::class: + $activity = StartMongodb::run($database); + break; + case \App\Models\StandaloneMysql::class: + $activity = StartMysql::run($database); + break; + case \App\Models\StandaloneMariadb::class: + $activity = StartMariadb::run($database); + break; + case \App\Models\StandaloneKeydb::class: + $activity = StartKeydb::run($database); + break; + case \App\Models\StandaloneDragonfly::class: + $activity = StartDragonfly::run($database); + break; + case \App\Models\StandaloneClickhouse::class: + $activity = StartClickhouse::run($database); + break; + } + if ($database->is_public && $database->public_port) { + StartDatabaseProxy::dispatch($database)->onQueue('high'); + } + + return $activity; + } +} diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php index a514c51b4..d7a3bc697 100644 --- a/app/Actions/Database/StartDatabaseProxy.php +++ b/app/Actions/Database/StartDatabaseProxy.php @@ -26,7 +26,7 @@ class StartDatabaseProxy $server = data_get($database, 'destination.server'); $containerName = data_get($database, 'uuid'); $proxyContainerName = "{$database->uuid}-proxy"; - if ($database->getMorphClass() === 'App\Models\ServiceDatabase') { + if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) { $databaseType = $database->databaseType(); // $connectPredefined = data_get($database, 'service.connect_to_docker_network'); $network = $database->service->uuid; @@ -34,54 +34,54 @@ class StartDatabaseProxy $proxyContainerName = "{$database->service->uuid}-proxy"; switch ($databaseType) { case 'standalone-mariadb': - $type = 'App\Models\StandaloneMariadb'; + $type = \App\Models\StandaloneMariadb::class; $containerName = "mariadb-{$database->service->uuid}"; break; case 'standalone-mongodb': - $type = 'App\Models\StandaloneMongodb'; + $type = \App\Models\StandaloneMongodb::class; $containerName = "mongodb-{$database->service->uuid}"; break; case 'standalone-mysql': - $type = 'App\Models\StandaloneMysql'; + $type = \App\Models\StandaloneMysql::class; $containerName = "mysql-{$database->service->uuid}"; break; case 'standalone-postgresql': - $type = 'App\Models\StandalonePostgresql'; + $type = \App\Models\StandalonePostgresql::class; $containerName = "postgresql-{$database->service->uuid}"; break; case 'standalone-redis': - $type = 'App\Models\StandaloneRedis'; + $type = \App\Models\StandaloneRedis::class; $containerName = "redis-{$database->service->uuid}"; break; case 'standalone-keydb': - $type = 'App\Models\StandaloneKeydb'; + $type = \App\Models\StandaloneKeydb::class; $containerName = "keydb-{$database->service->uuid}"; break; case 'standalone-dragonfly': - $type = 'App\Models\StandaloneDragonfly'; + $type = \App\Models\StandaloneDragonfly::class; $containerName = "dragonfly-{$database->service->uuid}"; break; case 'standalone-clickhouse': - $type = 'App\Models\StandaloneClickhouse'; + $type = \App\Models\StandaloneClickhouse::class; $containerName = "clickhouse-{$database->service->uuid}"; break; } } - if ($type === 'App\Models\StandaloneRedis') { + if ($type === \App\Models\StandaloneRedis::class) { $internalPort = 6379; - } elseif ($type === 'App\Models\StandalonePostgresql') { + } elseif ($type === \App\Models\StandalonePostgresql::class) { $internalPort = 5432; - } elseif ($type === 'App\Models\StandaloneMongodb') { + } elseif ($type === \App\Models\StandaloneMongodb::class) { $internalPort = 27017; - } elseif ($type === 'App\Models\StandaloneMysql') { + } elseif ($type === \App\Models\StandaloneMysql::class) { $internalPort = 3306; - } elseif ($type === 'App\Models\StandaloneMariadb') { + } elseif ($type === \App\Models\StandaloneMariadb::class) { $internalPort = 3306; - } elseif ($type === 'App\Models\StandaloneKeydb') { + } elseif ($type === \App\Models\StandaloneKeydb::class) { $internalPort = 6379; - } elseif ($type === 'App\Models\StandaloneDragonfly') { + } elseif ($type === \App\Models\StandaloneDragonfly::class) { $internalPort = 6379; - } elseif ($type === 'App\Models\StandaloneClickhouse') { + } elseif ($type === \App\Models\StandaloneClickhouse::class) { $internalPort = 9000; } $configuration_dir = database_proxy_dir($database->uuid); diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php index 19b1c5814..c72714e1c 100644 --- a/app/Actions/Database/StartDragonfly.php +++ b/app/Actions/Database/StartDragonfly.php @@ -3,7 +3,6 @@ namespace App\Actions\Database; use App\Models\StandaloneDragonfly; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; @@ -47,11 +46,10 @@ class StartDragonfly 'networks' => [ $this->database->destination->network, ], - 'ulimits' => [ - 'memlock' => '-1', - ], 'labels' => [ 'coolify.managed' => 'true', + 'coolify.type' => 'database', + 'coolify.databaseId' => $this->database->id, ], 'healthcheck' => [ 'test' => "redis-cli -a {$this->database->dragonfly_password} ping", @@ -80,14 +78,7 @@ class StartDragonfly data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { - $docker_compose['services'][$container_name]['logging'] = [ - 'driver' => 'fluentd', - 'options' => [ - 'fluentd-address' => 'tcp://127.0.0.1:24224', - 'fluentd-async' => 'true', - 'fluentd-sub-second-precision' => 'true', - ], - ]; + $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration(); } if (count($this->database->ports_mappings_array) > 0) { $docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array; @@ -103,6 +94,11 @@ class StartDragonfly if (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } + + // Add custom docker run options + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; @@ -155,7 +151,7 @@ class StartDragonfly $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('REDIS_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) { $environment_variables->push("REDIS_PASSWORD={$this->database->dragonfly_password}"); } diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php index a632f6e8c..bd98258ab 100644 --- a/app/Actions/Database/StartKeydb.php +++ b/app/Actions/Database/StartKeydb.php @@ -4,7 +4,6 @@ namespace App\Actions\Database; use App\Models\StandaloneKeydb; use Illuminate\Support\Facades\Storage; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; @@ -51,6 +50,8 @@ class StartKeydb ], 'labels' => [ 'coolify.managed' => 'true', + 'coolify.type' => 'database', + 'coolify.databaseId' => $this->database->id, ], 'healthcheck' => [ 'test' => "keydb-cli --pass {$this->database->keydb_password} ping", @@ -79,14 +80,7 @@ class StartKeydb data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { - $docker_compose['services'][$container_name]['logging'] = [ - 'driver' => 'fluentd', - 'options' => [ - 'fluentd-address' => 'tcp://127.0.0.1:24224', - 'fluentd-async' => 'true', - 'fluentd-sub-second-precision' => 'true', - ], - ]; + $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration(); } if (count($this->database->ports_mappings_array) > 0) { $docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array; @@ -111,6 +105,10 @@ class StartKeydb ]; $docker_compose['services'][$container_name]['command'] = "keydb-server /etc/keydb/keydb.conf --requirepass {$this->database->keydb_password} --appendonly yes"; } + + // Add custom docker run options + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; @@ -163,10 +161,12 @@ class StartKeydb $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('REDIS_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) { $environment_variables->push("REDIS_PASSWORD={$this->database->keydb_password}"); } + add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables); + return $environment_variables->all(); } diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php index 31d3f0640..696dd7ff4 100644 --- a/app/Actions/Database/StartMariadb.php +++ b/app/Actions/Database/StartMariadb.php @@ -3,7 +3,6 @@ namespace App\Actions\Database; use App\Models\StandaloneMariadb; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; @@ -46,6 +45,8 @@ class StartMariadb ], 'labels' => [ 'coolify.managed' => 'true', + 'coolify.type' => 'database', + 'coolify.databaseId' => $this->database->id, ], 'healthcheck' => [ 'test' => ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized'], @@ -74,14 +75,7 @@ class StartMariadb data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { - $docker_compose['services'][$container_name]['logging'] = [ - 'driver' => 'fluentd', - 'options' => [ - 'fluentd-address' => 'tcp://127.0.0.1:24224', - 'fluentd-async' => 'true', - 'fluentd-sub-second-precision' => 'true', - ], - ]; + $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration(); } if (count($this->database->ports_mappings_array) > 0) { $docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array; @@ -105,6 +99,11 @@ class StartMariadb 'read_only' => true, ]; } + + // Add custom docker run options + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; @@ -157,21 +156,23 @@ class StartMariadb $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) { $environment_variables->push("MARIADB_ROOT_PASSWORD={$this->database->mariadb_root_password}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_DATABASE'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_DATABASE'))->isEmpty()) { $environment_variables->push("MARIADB_DATABASE={$this->database->mariadb_database}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_USER'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_USER'))->isEmpty()) { $environment_variables->push("MARIADB_USER={$this->database->mariadb_user}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_PASSWORD'))->isEmpty()) { $environment_variables->push("MARIADB_PASSWORD={$this->database->mariadb_password}"); } + add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables); + return $environment_variables->all(); } diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index 8db34b20f..26a0f82d0 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -3,7 +3,6 @@ namespace App\Actions\Database; use App\Models\StandaloneMongodb; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; @@ -26,6 +25,10 @@ class StartMongodb $container_name = $this->database->uuid; $this->configuration_dir = database_configuration_dir().'/'.$container_name; + if (isDev()) { + $this->configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name; + } + $this->commands = [ "echo 'Starting {$database->name}.'", "mkdir -p $this->configuration_dir", @@ -50,6 +53,8 @@ class StartMongodb ], 'labels' => [ 'coolify.managed' => 'true', + 'coolify.type' => 'database', + 'coolify.databaseId' => $this->database->id, ], 'healthcheck' => [ 'test' => [ @@ -82,14 +87,7 @@ class StartMongodb data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { - $docker_compose['services'][$container_name]['logging'] = [ - 'driver' => 'fluentd', - 'options' => [ - 'fluentd-address' => 'tcp://127.0.0.1:24224', - 'fluentd-async' => 'true', - 'fluentd-sub-second-precision' => 'true', - ], - ]; + $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration(); } if (count($this->database->ports_mappings_array) > 0) { $docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array; @@ -122,6 +120,10 @@ class StartMongodb 'read_only' => true, ]; + // Add custom docker run options + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; @@ -174,18 +176,20 @@ class StartMongodb $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) { $environment_variables->push("MONGO_INITDB_ROOT_USERNAME={$this->database->mongo_initdb_root_username}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) { $environment_variables->push("MONGO_INITDB_ROOT_PASSWORD={$this->database->mongo_initdb_root_password}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) { $environment_variables->push("MONGO_INITDB_DATABASE={$this->database->mongo_initdb_database}"); } + add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables); + return $environment_variables->all(); } diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php index 8280faa56..a3694648f 100644 --- a/app/Actions/Database/StartMysql.php +++ b/app/Actions/Database/StartMysql.php @@ -3,7 +3,6 @@ namespace App\Actions\Database; use App\Models\StandaloneMysql; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; @@ -46,6 +45,8 @@ class StartMysql ], 'labels' => [ 'coolify.managed' => 'true', + 'coolify.type' => 'database', + 'coolify.databaseId' => $this->database->id, ], 'healthcheck' => [ 'test' => ['CMD', 'mysqladmin', 'ping', '-h', 'localhost', '-u', 'root', "-p{$this->database->mysql_root_password}"], @@ -74,14 +75,7 @@ class StartMysql data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { - $docker_compose['services'][$container_name]['logging'] = [ - 'driver' => 'fluentd', - 'options' => [ - 'fluentd-address' => 'tcp://127.0.0.1:24224', - 'fluentd-async' => 'true', - 'fluentd-sub-second-precision' => 'true', - ], - ]; + $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration(); } if (count($this->database->ports_mappings_array) > 0) { $docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array; @@ -105,6 +99,11 @@ class StartMysql 'read_only' => true, ]; } + + // Add custom docker run options + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; @@ -157,21 +156,23 @@ class StartMysql $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) { $environment_variables->push("MYSQL_ROOT_PASSWORD={$this->database->mysql_root_password}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_DATABASE'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_DATABASE'))->isEmpty()) { $environment_variables->push("MYSQL_DATABASE={$this->database->mysql_database}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_USER'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_USER'))->isEmpty()) { $environment_variables->push("MYSQL_USER={$this->database->mysql_user}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_PASSWORD'))->isEmpty()) { $environment_variables->push("MYSQL_PASSWORD={$this->database->mysql_password}"); } + add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables); + return $environment_variables->all(); } diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index 23b9742c7..f5e85087f 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -3,7 +3,6 @@ namespace App\Actions\Database; use App\Models\StandalonePostgresql; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; @@ -50,6 +49,8 @@ class StartPostgresql ], 'labels' => [ 'coolify.managed' => 'true', + 'coolify.type' => 'database', + 'coolify.databaseId' => $this->database->id, ], 'healthcheck' => [ 'test' => [ @@ -81,14 +82,7 @@ class StartPostgresql data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { - $docker_compose['services'][$container_name]['logging'] = [ - 'driver' => 'fluentd', - 'options' => [ - 'fluentd-address' => 'tcp://127.0.0.1:24224', - 'fluentd-async' => 'true', - 'fluentd-sub-second-precision' => 'true', - ], - ]; + $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration(); } if (count($this->database->ports_mappings_array) > 0) { $docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array; @@ -127,6 +121,10 @@ class StartPostgresql 'config_file=/etc/postgresql/postgresql.conf', ]; } + // Add custom docker run options + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; @@ -179,21 +177,23 @@ class StartPostgresql $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_USER'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('POSTGRES_USER'))->isEmpty()) { $environment_variables->push("POSTGRES_USER={$this->database->postgres_user}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PGUSER'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('PGUSER'))->isEmpty()) { $environment_variables->push("PGUSER={$this->database->postgres_user}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('POSTGRES_PASSWORD'))->isEmpty()) { $environment_variables->push("POSTGRES_PASSWORD={$this->database->postgres_password}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_DB'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('POSTGRES_DB'))->isEmpty()) { $environment_variables->push("POSTGRES_DB={$this->database->postgres_db}"); } + add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables); + return $environment_variables->all(); } diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php index 065df5e52..7a2d2b34d 100644 --- a/app/Actions/Database/StartRedis.php +++ b/app/Actions/Database/StartRedis.php @@ -4,7 +4,6 @@ namespace App\Actions\Database; use App\Models\StandaloneRedis; use Illuminate\Support\Facades\Storage; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; @@ -22,8 +21,6 @@ class StartRedis { $this->database = $database; - $startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes"; - $container_name = $this->database->uuid; $this->configuration_dir = database_configuration_dir().'/'.$container_name; @@ -38,6 +35,8 @@ class StartRedis $environment_variables = $this->generate_environment_variables(); $this->add_custom_redis(); + $startCommand = $this->buildStartCommand(); + $docker_compose = [ 'services' => [ $container_name => [ @@ -51,6 +50,8 @@ class StartRedis ], 'labels' => [ 'coolify.managed' => 'true', + 'coolify.type' => 'database', + 'coolify.databaseId' => $this->database->id, ], 'healthcheck' => [ 'test' => [ @@ -83,14 +84,7 @@ class StartRedis data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); } if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { - $docker_compose['services'][$container_name]['logging'] = [ - 'driver' => 'fluentd', - 'options' => [ - 'fluentd-address' => 'tcp://127.0.0.1:24224', - 'fluentd-async' => 'true', - 'fluentd-sub-second-precision' => 'true', - ], - ]; + $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration(); } if (count($this->database->ports_mappings_array) > 0) { $docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array; @@ -113,8 +107,12 @@ class StartRedis 'target' => '/usr/local/etc/redis/redis.conf', 'read_only' => true, ]; - $docker_compose['services'][$container_name]['command'] = "redis-server /usr/local/etc/redis/redis.conf --requirepass {$this->database->redis_password} --appendonly yes"; } + + // Add custom docker run options + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; @@ -163,17 +161,54 @@ class StartRedis private function generate_environment_variables() { $environment_variables = collect(); + foreach ($this->database->runtime_environment_variables as $env) { - $environment_variables->push("$env->key=$env->real_value"); + if ($env->is_shared) { + $environment_variables->push("$env->key=$env->real_value"); + + if ($env->key === 'REDIS_PASSWORD') { + $this->database->update(['redis_password' => $env->real_value]); + } + + if ($env->key === 'REDIS_USERNAME') { + $this->database->update(['redis_username' => $env->real_value]); + } + } else { + if ($env->key === 'REDIS_PASSWORD') { + $env->update(['value' => $this->database->redis_password]); + } elseif ($env->key === 'REDIS_USERNAME') { + $env->update(['value' => $this->database->redis_username]); + } + $environment_variables->push("$env->key=$env->real_value"); + } } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('REDIS_PASSWORD'))->isEmpty()) { - $environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}"); - } + add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables); return $environment_variables->all(); } + private function buildStartCommand(): string + { + $hasRedisConf = ! is_null($this->database->redis_conf) && ! empty($this->database->redis_conf); + $redisConfPath = '/usr/local/etc/redis/redis.conf'; + + if ($hasRedisConf) { + $confContent = $this->database->redis_conf; + $hasRequirePass = str_contains($confContent, 'requirepass'); + + if ($hasRequirePass) { + $command = "redis-server $redisConfPath"; + } else { + $command = "redis-server $redisConfPath --requirepass {$this->database->redis_password}"; + } + } else { + $command = "redis-server --requirepass {$this->database->redis_password} --appendonly yes"; + } + + return $command; + } + private function add_custom_redis() { if (is_null($this->database->redis_conf) || empty($this->database->redis_conf)) { diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php index 66a32e811..e4cea7cee 100644 --- a/app/Actions/Database/StopDatabase.php +++ b/app/Actions/Database/StopDatabase.php @@ -2,6 +2,7 @@ namespace App\Actions\Database; +use App\Actions\Server\CleanupDocker; use App\Models\StandaloneClickhouse; use App\Models\StandaloneDragonfly; use App\Models\StandaloneKeydb; @@ -10,26 +11,65 @@ use App\Models\StandaloneMongodb; use App\Models\StandaloneMysql; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; +use Illuminate\Support\Facades\Process; use Lorisleiva\Actions\Concerns\AsAction; class StopDatabase { use AsAction; - public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database) + public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database, bool $isDeleteOperation = false, bool $dockerCleanup = true) { $server = $database->destination->server; if (! $server->isFunctional()) { return 'Server is not functional'; } - instant_remote_process( - ["docker rm -f {$database->uuid}"], - $server - ); + + $this->stopContainer($database, $database->uuid, 300); + if (! $isDeleteOperation) { + if ($dockerCleanup) { + CleanupDocker::dispatch($server, true); + } + } + if ($database->is_public) { StopDatabaseProxy::run($database); } - // TODO: make notification for services - // $database->environment->project->team->notify(new StatusChanged($database)); + + return 'Database stopped successfully'; + } + + private function stopContainer($database, string $containerName, int $timeout = 300): void + { + $server = $database->destination->server; + + $process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); + + $startTime = time(); + while ($process->running()) { + if (time() - $startTime >= $timeout) { + $this->forceStopContainer($containerName, $server); + break; + } + usleep(100000); + } + + $this->removeContainer($containerName, $server); + } + + private function forceStopContainer(string $containerName, $server): void + { + instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false); + } + + private function removeContainer(string $containerName, $server): void + { + instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false); + } + + private function deleteConnectedNetworks($uuid, $server) + { + instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false); + instant_remote_process(["docker network rm {$uuid}"], $server, false); } } diff --git a/app/Actions/Database/StopDatabaseProxy.php b/app/Actions/Database/StopDatabaseProxy.php index 984225435..0a166d24a 100644 --- a/app/Actions/Database/StopDatabaseProxy.php +++ b/app/Actions/Database/StopDatabaseProxy.php @@ -2,6 +2,7 @@ namespace App\Actions\Database; +use App\Events\DatabaseProxyStopped; use App\Models\ServiceDatabase; use App\Models\StandaloneClickhouse; use App\Models\StandaloneDragonfly; @@ -21,12 +22,16 @@ class StopDatabaseProxy { $server = data_get($database, 'destination.server'); $uuid = $database->uuid; - if ($database->getMorphClass() === 'App\Models\ServiceDatabase') { + if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) { $uuid = $database->service->uuid; $server = data_get($database, 'service.server'); } instant_remote_process(["docker rm -f {$uuid}-proxy"], $server); + $database->is_public = false; $database->save(); + + DatabaseProxyStopped::dispatch(); + } } diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php index 9b32e89f3..a08056837 100644 --- a/app/Actions/Docker/GetContainersStatus.php +++ b/app/Actions/Docker/GetContainersStatus.php @@ -3,15 +3,13 @@ namespace App\Actions\Docker; use App\Actions\Database\StartDatabaseProxy; -use App\Actions\Proxy\CheckProxy; -use App\Actions\Proxy\StartProxy; use App\Actions\Shared\ComplexStatusCheck; use App\Models\ApplicationPreview; use App\Models\Server; use App\Models\ServiceDatabase; use App\Notifications\Container\ContainerRestarted; -use App\Notifications\Container\ContainerStopped; use Illuminate\Support\Arr; +use Illuminate\Support\Collection; use Lorisleiva\Actions\Concerns\AsAction; class GetContainersStatus @@ -20,16 +18,19 @@ class GetContainersStatus public $applications; + public ?Collection $containers; + + public ?Collection $containerReplicates; + public $server; - public function handle(Server $server) + public function handle(Server $server, ?Collection $containers = null, ?Collection $containerReplicates = null) { - // if (isDev()) { - // $server = Server::find(0); - // } + $this->containers = $containers; + $this->containerReplicates = $containerReplicates; $this->server = $server; if (! $this->server->isFunctional()) { - return 'Server is not ready.'; + return 'Server is not functional.'; } $this->applications = $this->server->applications(); $skip_these_applications = collect([]); @@ -45,343 +46,18 @@ class GetContainersStatus $this->applications = $this->applications->filter(function ($value, $key) use ($skip_these_applications) { return ! $skip_these_applications->pluck('id')->contains($value->id); }); - $this->old_way(); - // if ($this->server->isSwarm()) { - // $this->old_way(); - // } else { - // if (!$this->server->is_metrics_enabled) { - // $this->old_way(); - // return; - // } - // $sentinel_found = instant_remote_process(["docker inspect coolify-sentinel"], $this->server, false); - // $sentinel_found = json_decode($sentinel_found, true); - // $status = data_get($sentinel_found, '0.State.Status', 'exited'); - // if ($status === 'running') { - // ray('Checking with Sentinel'); - // $this->sentinel(); - // } else { - // ray('Checking the Old way'); - // $this->old_way(); - // } - // } - } - - private function sentinel() - { - try { - $containers = $this->server->getContainers(); - if ($containers->count() === 0) { - return; - } - $databases = $this->server->databases(); - $services = $this->server->services()->get(); - $previews = $this->server->previews(); - $foundApplications = []; - $foundApplicationPreviews = []; - $foundDatabases = []; - $foundServices = []; - - foreach ($containers as $container) { - $labels = Arr::undot(data_get($container, 'labels')); - $containerStatus = data_get($container, 'state'); - $containerHealth = data_get($container, 'health_status', 'unhealthy'); - $containerStatus = "$containerStatus ($containerHealth)"; - $applicationId = data_get($labels, 'coolify.applicationId'); - if ($applicationId) { - $pullRequestId = data_get($labels, 'coolify.pullRequestId'); - if ($pullRequestId) { - if (str($applicationId)->contains('-')) { - $applicationId = str($applicationId)->before('-'); - } - $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first(); - if ($preview) { - $foundApplicationPreviews[] = $preview->id; - $statusFromDb = $preview->status; - if ($statusFromDb !== $containerStatus) { - $preview->update(['status' => $containerStatus]); - } - } else { - //Notify user that this container should not be there. - } - } else { - $application = $this->applications->where('id', $applicationId)->first(); - if ($application) { - $foundApplications[] = $application->id; - $statusFromDb = $application->status; - if ($statusFromDb !== $containerStatus) { - $application->update(['status' => $containerStatus]); - } - } else { - //Notify user that this container should not be there. - } - } - } else { - $uuid = data_get($labels, 'com.docker.compose.service'); - $type = data_get($labels, 'coolify.type'); - if ($uuid) { - if ($type === 'service') { - $database_id = data_get($labels, 'coolify.service.subId'); - if ($database_id) { - $service_db = ServiceDatabase::where('id', $database_id)->first(); - if ($service_db) { - $uuid = $service_db->service->uuid; - $isPublic = data_get($service_db, 'is_public'); - if ($isPublic) { - $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) { - if ($this->server->isSwarm()) { - // TODO: fix this with sentinel - return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; - } else { - return data_get($value, 'name') === "$uuid-proxy"; - } - })->first(); - if (! $foundTcpProxy) { - StartDatabaseProxy::run($service_db); - // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server)); - } - } - } - } - } else { - $database = $databases->where('uuid', $uuid)->first(); - if ($database) { - $isPublic = data_get($database, 'is_public'); - $foundDatabases[] = $database->id; - $statusFromDb = $database->status; - if ($statusFromDb !== $containerStatus) { - $database->update(['status' => $containerStatus]); - } - if ($isPublic) { - $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) { - if ($this->server->isSwarm()) { - // TODO: fix this with sentinel - return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; - } else { - return data_get($value, 'name') === "$uuid-proxy"; - } - })->first(); - if (! $foundTcpProxy) { - StartDatabaseProxy::run($database); - $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); - } - } - } else { - // Notify user that this container should not be there. - } - } - } - if (data_get($container, 'name') === 'coolify-db') { - $foundDatabases[] = 0; - } - } - $serviceLabelId = data_get($labels, 'coolify.serviceId'); - if ($serviceLabelId) { - $subType = data_get($labels, 'coolify.service.subType'); - $subId = data_get($labels, 'coolify.service.subId'); - $service = $services->where('id', $serviceLabelId)->first(); - if (! $service) { - continue; - } - if ($subType === 'application') { - $service = $service->applications()->where('id', $subId)->first(); - } else { - $service = $service->databases()->where('id', $subId)->first(); - } - if ($service) { - $foundServices[] = "$service->id-$service->name"; - $statusFromDb = $service->status; - if ($statusFromDb !== $containerStatus) { - // ray('Updating status: ' . $containerStatus); - $service->update(['status' => $containerStatus]); - } - } - } - } - $exitedServices = collect([]); - foreach ($services as $service) { - $apps = $service->applications()->get(); - $dbs = $service->databases()->get(); - foreach ($apps as $app) { - if (in_array("$app->id-$app->name", $foundServices)) { - continue; - } else { - $exitedServices->push($app); - } - } - foreach ($dbs as $db) { - if (in_array("$db->id-$db->name", $foundServices)) { - continue; - } else { - $exitedServices->push($db); - } - } - } - $exitedServices = $exitedServices->unique('id'); - foreach ($exitedServices as $exitedService) { - if (str($exitedService->status)->startsWith('exited')) { - continue; - } - $name = data_get($exitedService, 'name'); - $fqdn = data_get($exitedService, 'fqdn'); - if ($name) { - if ($fqdn) { - $containerName = "$name, available at $fqdn"; - } else { - $containerName = $name; - } - } else { - if ($fqdn) { - $containerName = $fqdn; - } else { - $containerName = null; - } - } - $projectUuid = data_get($service, 'environment.project.uuid'); - $serviceUuid = data_get($service, 'uuid'); - $environmentName = data_get($service, 'environment.name'); - - if ($projectUuid && $serviceUuid && $environmentName) { - $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/service/'.$serviceUuid; - } else { - $url = null; - } - // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - $exitedService->update(['status' => 'exited']); - } - - $notRunningApplications = $this->applications->pluck('id')->diff($foundApplications); - foreach ($notRunningApplications as $applicationId) { - $application = $this->applications->where('id', $applicationId)->first(); - if (str($application->status)->startsWith('exited')) { - continue; - } - $application->update(['status' => 'exited']); - - $name = data_get($application, 'name'); - $fqdn = data_get($application, 'fqdn'); - - $containerName = $name ? "$name ($fqdn)" : $fqdn; - - $projectUuid = data_get($application, 'environment.project.uuid'); - $applicationUuid = data_get($application, 'uuid'); - $environment = data_get($application, 'environment.name'); - - if ($projectUuid && $applicationUuid && $environment) { - $url = base_url().'/project/'.$projectUuid.'/'.$environment.'/application/'.$applicationUuid; - } else { - $url = null; - } - - // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - } - $notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews); - foreach ($notRunningApplicationPreviews as $previewId) { - $preview = $previews->where('id', $previewId)->first(); - if (str($preview->status)->startsWith('exited')) { - continue; - } - $preview->update(['status' => 'exited']); - - $name = data_get($preview, 'name'); - $fqdn = data_get($preview, 'fqdn'); - - $containerName = $name ? "$name ($fqdn)" : $fqdn; - - $projectUuid = data_get($preview, 'application.environment.project.uuid'); - $environmentName = data_get($preview, 'application.environment.name'); - $applicationUuid = data_get($preview, 'application.uuid'); - - if ($projectUuid && $applicationUuid && $environmentName) { - $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid; - } else { - $url = null; - } - - // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - } - $notRunningDatabases = $databases->pluck('id')->diff($foundDatabases); - foreach ($notRunningDatabases as $database) { - $database = $databases->where('id', $database)->first(); - if (str($database->status)->startsWith('exited')) { - continue; - } - $database->update(['status' => 'exited']); - - $name = data_get($database, 'name'); - $fqdn = data_get($database, 'fqdn'); - - $containerName = $name; - - $projectUuid = data_get($database, 'environment.project.uuid'); - $environmentName = data_get($database, 'environment.name'); - $databaseUuid = data_get($database, 'uuid'); - - if ($projectUuid && $databaseUuid && $environmentName) { - $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/database/'.$databaseUuid; - } else { - $url = null; - } - // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - } - - // Check if proxy is running - $this->server->proxyType(); - $foundProxyContainer = $containers->filter(function ($value, $key) { - if ($this->server->isSwarm()) { - // TODO: fix this with sentinel - return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; - } else { - return data_get($value, 'name') === 'coolify-proxy'; - } - })->first(); - if (! $foundProxyContainer) { - try { - $shouldStart = CheckProxy::run($this->server); - if ($shouldStart) { - StartProxy::run($this->server, false); - $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); - } - } catch (\Throwable $e) { - ray($e); - } - } else { - $this->server->proxy->status = data_get($foundProxyContainer, 'state'); - $this->server->save(); - $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); - instant_remote_process($connectProxyToDockerNetworks, $this->server, false); - } - } catch (\Exception $e) { - // send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage()); - ray($e->getMessage()); - - return handleError($e); + if ($this->containers === null) { + ['containers' => $this->containers, 'containerReplicates' => $this->containerReplicates] = $this->server->getContainers(); } - } - private function old_way() - { - if ($this->server->isSwarm()) { - $containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false); - $containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false); - } else { - // Precheck for containers - $containers = instant_remote_process(['docker container ls -q'], $this->server, false); - if (! $containers) { - return; - } - $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false); - $containerReplicates = null; - } - if (is_null($containers)) { + if (is_null($this->containers)) { return; } - $containers = format_docker_command_output_to_json($containers); - if ($containerReplicates) { - $containerReplicates = format_docker_command_output_to_json($containerReplicates); - foreach ($containerReplicates as $containerReplica) { + if ($this->containerReplicates) { + foreach ($this->containerReplicates as $containerReplica) { $name = data_get($containerReplica, 'Name'); - $containers = $containers->map(function ($container) use ($name, $containerReplica) { + $this->containers = $this->containers->map(function ($container) use ($name, $containerReplica) { if (data_get($container, 'Spec.Name') === $name) { $replicas = data_get($containerReplica, 'Replicas'); $running = str($replicas)->explode('/')[0]; @@ -407,7 +83,7 @@ class GetContainersStatus $foundDatabases = []; $foundServices = []; - foreach ($containers as $container) { + foreach ($this->containers as $container) { if ($this->server->isSwarm()) { $labels = data_get($container, 'Spec.Labels'); $uuid = data_get($labels, 'coolify.name'); @@ -431,6 +107,8 @@ class GetContainersStatus $statusFromDb = $preview->status; if ($statusFromDb !== $containerStatus) { $preview->update(['status' => $containerStatus]); + } else { + $preview->update(['last_online_at' => now()]); } } else { //Notify user that this container should not be there. @@ -442,6 +120,8 @@ class GetContainersStatus $statusFromDb = $application->status; if ($statusFromDb !== $containerStatus) { $application->update(['status' => $containerStatus]); + } else { + $application->update(['last_online_at' => now()]); } } else { //Notify user that this container should not be there. @@ -461,7 +141,7 @@ class GetContainersStatus if ($uuid) { $isPublic = data_get($service_db, 'is_public'); if ($isPublic) { - $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) { + $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { if ($this->server->isSwarm()) { return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; } else { @@ -484,9 +164,12 @@ class GetContainersStatus $statusFromDb = $database->status; if ($statusFromDb !== $containerStatus) { $database->update(['status' => $containerStatus]); + } else { + $database->update(['last_online_at' => now()]); } + if ($isPublic) { - $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) { + $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { if ($this->server->isSwarm()) { return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; } else { @@ -495,7 +178,7 @@ class GetContainersStatus })->first(); if (! $foundTcpProxy) { StartDatabaseProxy::run($database); - $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); + // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); } } } else { @@ -526,6 +209,8 @@ class GetContainersStatus if ($statusFromDb !== $containerStatus) { // ray('Updating status: ' . $containerStatus); $service->update(['status' => $containerStatus]); + } else { + $service->update(['last_online_at' => now()]); } } } @@ -549,7 +234,7 @@ class GetContainersStatus } } } - $exitedServices = $exitedServices->unique('id'); + $exitedServices = $exitedServices->unique('uuid'); foreach ($exitedServices as $exitedService) { if (str($exitedService->status)->startsWith('exited')) { continue; @@ -656,31 +341,5 @@ class GetContainersStatus } // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); } - - // Check if proxy is running - $this->server->proxyType(); - $foundProxyContainer = $containers->filter(function ($value, $key) { - if ($this->server->isSwarm()) { - return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; - } else { - return data_get($value, 'Name') === '/coolify-proxy'; - } - })->first(); - if (! $foundProxyContainer) { - try { - $shouldStart = CheckProxy::run($this->server); - if ($shouldStart) { - StartProxy::run($this->server, false); - $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); - } - } catch (\Throwable $e) { - ray($e); - } - } else { - $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); - $this->server->save(); - $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); - instant_remote_process($connectProxyToDockerNetworks, $this->server, false); - } } } diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php index f8882d12a..ea2befd3a 100644 --- a/app/Actions/Fortify/CreateNewUser.php +++ b/app/Actions/Fortify/CreateNewUser.php @@ -2,17 +2,15 @@ namespace App\Actions\Fortify; -use App\Models\InstanceSettings; use App\Models\User; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; +use Illuminate\Validation\Rules\Password; use Laravel\Fortify\Contracts\CreatesNewUsers; class CreateNewUser implements CreatesNewUsers { - use PasswordValidationRules; - /** * Validate and create a newly registered user. * @@ -20,7 +18,7 @@ class CreateNewUser implements CreatesNewUsers */ public function create(array $input): User { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); if (! $settings->is_registration_enabled) { abort(403); } @@ -33,7 +31,7 @@ class CreateNewUser implements CreatesNewUsers 'max:255', Rule::unique(User::class), ], - 'password' => $this->passwordRules(), + 'password' => ['required', Password::defaults(), 'confirmed'], ])->validate(); if (User::count() == 0) { @@ -42,19 +40,19 @@ class CreateNewUser implements CreatesNewUsers $user = User::create([ 'id' => 0, 'name' => $input['name'], - 'email' => $input['email'], + 'email' => strtolower($input['email']), 'password' => Hash::make($input['password']), ]); $team = $user->teams()->first(); // Disable registration after first user is created - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $settings->is_registration_enabled = false; $settings->save(); } else { $user = User::create([ 'name' => $input['name'], - 'email' => $input['email'], + 'email' => strtolower($input['email']), 'password' => Hash::make($input['password']), ]); $team = $user->teams()->first(); diff --git a/app/Actions/Fortify/PasswordValidationRules.php b/app/Actions/Fortify/PasswordValidationRules.php deleted file mode 100644 index 92fcc7532..000000000 --- a/app/Actions/Fortify/PasswordValidationRules.php +++ /dev/null @@ -1,18 +0,0 @@ - - */ - protected function passwordRules(): array - { - return ['required', 'string', new Password, 'confirmed']; - } -} diff --git a/app/Actions/Fortify/ResetUserPassword.php b/app/Actions/Fortify/ResetUserPassword.php index 7a57c5037..d3727a52c 100644 --- a/app/Actions/Fortify/ResetUserPassword.php +++ b/app/Actions/Fortify/ResetUserPassword.php @@ -5,12 +5,11 @@ namespace App\Actions\Fortify; use App\Models\User; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\Rules\Password; use Laravel\Fortify\Contracts\ResetsUserPasswords; class ResetUserPassword implements ResetsUserPasswords { - use PasswordValidationRules; - /** * Validate and reset the user's forgotten password. * @@ -19,7 +18,7 @@ class ResetUserPassword implements ResetsUserPasswords public function reset(User $user, array $input): void { Validator::make($input, [ - 'password' => $this->passwordRules(), + 'password' => ['required', Password::defaults(), 'confirmed'], ])->validate(); $user->forceFill([ diff --git a/app/Actions/Fortify/UpdateUserPassword.php b/app/Actions/Fortify/UpdateUserPassword.php index 700563905..0c51ec56d 100644 --- a/app/Actions/Fortify/UpdateUserPassword.php +++ b/app/Actions/Fortify/UpdateUserPassword.php @@ -5,12 +5,11 @@ namespace App\Actions\Fortify; use App\Models\User; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\Rules\Password; use Laravel\Fortify\Contracts\UpdatesUserPasswords; class UpdateUserPassword implements UpdatesUserPasswords { - use PasswordValidationRules; - /** * Validate and update the user's password. * @@ -20,7 +19,7 @@ class UpdateUserPassword implements UpdatesUserPasswords { Validator::make($input, [ 'current_password' => ['required', 'string', 'current_password:web'], - 'password' => $this->passwordRules(), + 'password' => ['required', Password::defaults(), 'confirmed'], ], [ 'current_password.current_password' => __('The provided password does not match your current password.'), ])->validateWithBag('updatePassword'); diff --git a/app/Actions/License/CheckResaleLicense.php b/app/Actions/License/CheckResaleLicense.php index dcb4058c0..26a1ff7bf 100644 --- a/app/Actions/License/CheckResaleLicense.php +++ b/app/Actions/License/CheckResaleLicense.php @@ -2,7 +2,6 @@ namespace App\Actions\License; -use App\Models\InstanceSettings; use Illuminate\Support\Facades\Http; use Lorisleiva\Actions\Concerns\AsAction; @@ -13,7 +12,7 @@ class CheckResaleLicense public function handle() { try { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); if (isDev()) { $settings->update([ 'is_resale_license_active' => true, @@ -26,8 +25,6 @@ class CheckResaleLicense // } $base_url = config('coolify.license_url'); $instance_id = config('app.id'); - - ray("Checking license key against $base_url/lemon/validate"); $data = Http::withHeaders([ 'Accept' => 'application/json', ])->get("$base_url/lemon/validate", [ @@ -35,7 +32,6 @@ class CheckResaleLicense 'instance_id' => $instance_id, ])->json(); if (data_get($data, 'valid') === true && data_get($data, 'license_key.status') === 'active') { - ray('Valid & active license key'); $settings->update([ 'is_resale_license_active' => true, ]); @@ -49,7 +45,6 @@ class CheckResaleLicense 'instance_id' => $instance_id, ])->json(); if (data_get($data, 'activated') === true) { - ray('Activated license key'); $settings->update([ 'is_resale_license_active' => true, ]); @@ -61,7 +56,6 @@ class CheckResaleLicense } throw new \Exception('Cannot activate license key.'); } catch (\Throwable $e) { - ray($e); $settings->update([ 'resale_license' => null, 'is_resale_license_active' => false, diff --git a/app/Actions/Proxy/CheckConfiguration.php b/app/Actions/Proxy/CheckConfiguration.php index 35374ba43..bdeafd061 100644 --- a/app/Actions/Proxy/CheckConfiguration.php +++ b/app/Actions/Proxy/CheckConfiguration.php @@ -3,7 +3,6 @@ namespace App\Actions\Proxy; use App\Models\Server; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; class CheckConfiguration @@ -22,9 +21,8 @@ class CheckConfiguration "cat $proxy_path/docker-compose.yml", ]; $proxy_configuration = instant_remote_process($payload, $server, false); - if ($reset || ! $proxy_configuration || is_null($proxy_configuration)) { - $proxy_configuration = Str::of(generate_default_proxy_configuration($server))->trim()->value; + $proxy_configuration = str(generate_default_proxy_configuration($server))->trim()->value(); } if (! $proxy_configuration || is_null($proxy_configuration)) { throw new \Exception('Could not generate proxy configuration'); diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php index 735b972af..51303d87a 100644 --- a/app/Actions/Proxy/CheckProxy.php +++ b/app/Actions/Proxy/CheckProxy.php @@ -2,14 +2,18 @@ namespace App\Actions\Proxy; +use App\Enums\ProxyTypes; use App\Models\Server; +use Illuminate\Support\Facades\Log; use Lorisleiva\Actions\Concerns\AsAction; +use Symfony\Component\Yaml\Yaml; class CheckProxy { use AsAction; - public function handle(Server $server, $fromUI = false) + // It should return if the proxy should be started (true) or not (false) + public function handle(Server $server, $fromUI = false): bool { if (! $server->isFunctional()) { return false; @@ -26,7 +30,7 @@ class CheckProxy if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) { return false; } - ['uptime' => $uptime, 'error' => $error] = $server->validateConnection(); + ['uptime' => $uptime, 'error' => $error] = $server->validateConnection(false); if (! $uptime) { throw new \Exception($error); } @@ -62,22 +66,42 @@ class CheckProxy $ip = 'host.docker.internal'; } - $connection80 = @fsockopen($ip, '80'); - $connection443 = @fsockopen($ip, '443'); - $port80 = is_resource($connection80) && fclose($connection80); - $port443 = is_resource($connection443) && fclose($connection443); - if ($port80) { - if ($fromUI) { - throw new \Exception("Port 80 is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord"); + $portsToCheck = ['80', '443']; + + try { + if ($server->proxyType() !== ProxyTypes::NONE->value) { + $proxyCompose = CheckConfiguration::run($server); + if (isset($proxyCompose)) { + $yaml = Yaml::parse($proxyCompose); + $portsToCheck = []; + if ($server->proxyType() === ProxyTypes::TRAEFIK->value) { + $ports = data_get($yaml, 'services.traefik.ports'); + } elseif ($server->proxyType() === ProxyTypes::CADDY->value) { + $ports = data_get($yaml, 'services.caddy.ports'); + } + if (isset($ports)) { + foreach ($ports as $port) { + $portsToCheck[] = str($port)->before(':')->value(); + } + } + } } else { - return false; + $portsToCheck = []; } + } catch (\Exception $e) { + Log::error('Error checking proxy: '.$e->getMessage()); } - if ($port443) { - if ($fromUI) { - throw new \Exception("Port 443 is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord"); - } else { - return false; + if (count($portsToCheck) === 0) { + return false; + } + foreach ($portsToCheck as $port) { + $connection = @fsockopen($ip, $port); + if (is_resource($connection) && fclose($connection)) { + if ($fromUI) { + throw new \Exception("Port $port is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord"); + } else { + return false; + } } } diff --git a/app/Actions/Proxy/SaveConfiguration.php b/app/Actions/Proxy/SaveConfiguration.php index 4c413ca36..f2de2b3f5 100644 --- a/app/Actions/Proxy/SaveConfiguration.php +++ b/app/Actions/Proxy/SaveConfiguration.php @@ -3,7 +3,6 @@ namespace App\Actions\Proxy; use App\Models\Server; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; class SaveConfiguration @@ -18,7 +17,7 @@ class SaveConfiguration $proxy_path = $server->proxyPath(); $docker_compose_yml_base64 = base64_encode($proxy_settings); - $server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value; + $server->proxy->last_saved_settings = str($docker_compose_yml_base64)->pipe('md5')->value; $server->save(); return instant_remote_process([ diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index 710b5cdd8..7c93720cb 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -4,7 +4,6 @@ namespace App\Actions\Proxy; use App\Events\ProxyStarted; use App\Models\Server; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; use Spatie\Activitylog\Models\Activity; @@ -12,66 +11,62 @@ class StartProxy { use AsAction; - public function handle(Server $server, bool $async = true): string|Activity + public function handle(Server $server, bool $async = true, bool $force = false): string|Activity { - try { - $proxyType = $server->proxyType(); - if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop || $server->isBuildServer()) { - return 'OK'; - } - $commands = collect([]); - $proxy_path = $server->proxyPath(); - $configuration = CheckConfiguration::run($server); - if (! $configuration) { - throw new \Exception('Configuration is not synced'); - } - SaveConfiguration::run($server, $configuration); - $docker_compose_yml_base64 = base64_encode($configuration); - $server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value; + $proxyType = $server->proxyType(); + if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop || $server->isBuildServer()) && $force === false) { + return 'OK'; + } + $commands = collect([]); + $proxy_path = $server->proxyPath(); + $configuration = CheckConfiguration::run($server); + if (! $configuration) { + throw new \Exception('Configuration is not synced'); + } + SaveConfiguration::run($server, $configuration); + $docker_compose_yml_base64 = base64_encode($configuration); + $server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value(); + $server->save(); + if ($server->isSwarm()) { + $commands = $commands->merge([ + "mkdir -p $proxy_path/dynamic", + "cd $proxy_path", + "echo 'Creating required Docker Compose file.'", + "echo 'Starting coolify-proxy.'", + 'docker stack deploy -c docker-compose.yml coolify-proxy', + "echo 'Successfully started coolify-proxy.'", + ]); + } else { + $caddfile = 'import /dynamic/*.caddy'; + $commands = $commands->merge([ + "mkdir -p $proxy_path/dynamic", + "cd $proxy_path", + "echo '$caddfile' > $proxy_path/dynamic/Caddyfile", + "echo 'Creating required Docker Compose file.'", + "echo 'Pulling docker image.'", + 'docker compose pull', + 'if docker ps -a --format "{{.Names}}" | grep -q "^coolify-proxy$"; then', + " echo 'Stopping and removing existing coolify-proxy.'", + ' docker rm -f coolify-proxy || true', + " echo 'Successfully stopped and removed existing coolify-proxy.'", + 'fi', + "echo 'Starting coolify-proxy.'", + 'docker compose up -d --remove-orphans', + "echo 'Successfully started coolify-proxy.'", + ]); + $commands = $commands->merge(connectProxyToNetworks($server)); + } + + if ($async) { + return remote_process($commands, $server, callEventOnFinish: 'ProxyStarted', callEventData: $server); + } else { + instant_remote_process($commands, $server); + $server->proxy->set('status', 'running'); + $server->proxy->set('type', $proxyType); $server->save(); - if ($server->isSwarm()) { - $commands = $commands->merge([ - "mkdir -p $proxy_path/dynamic", - "cd $proxy_path", - "echo 'Creating required Docker Compose file.'", - "echo 'Starting coolify-proxy.'", - 'docker stack deploy -c docker-compose.yml coolify-proxy', - "echo 'Proxy started successfully.'", - ]); - } else { - $caddfile = 'import /dynamic/*.caddy'; - $commands = $commands->merge([ - "mkdir -p $proxy_path/dynamic", - "cd $proxy_path", - "echo '$caddfile' > $proxy_path/dynamic/Caddyfile", - "echo 'Creating required Docker Compose file.'", - "echo 'Pulling docker image.'", - 'docker compose pull', - "echo 'Stopping existing coolify-proxy.'", - 'docker compose down -v --remove-orphans > /dev/null 2>&1', - "echo 'Starting coolify-proxy.'", - 'docker compose up -d --remove-orphans', - "echo 'Proxy started successfully.'", - ]); - $commands = $commands->merge(connectProxyToNetworks($server)); - } + ProxyStarted::dispatch($server); - if ($async) { - $activity = remote_process($commands, $server, callEventOnFinish: 'ProxyStarted', callEventData: $server); - - return $activity; - } else { - instant_remote_process($commands, $server); - $server->proxy->set('status', 'running'); - $server->proxy->set('type', $proxyType); - $server->save(); - ProxyStarted::dispatch($server); - - return 'OK'; - } - } catch (\Throwable $e) { - ray($e); - throw $e; + return 'OK'; } } } diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php index 1261e6830..dc6ac12bf 100644 --- a/app/Actions/Server/CleanupDocker.php +++ b/app/Actions/Server/CleanupDocker.php @@ -9,16 +9,31 @@ class CleanupDocker { use AsAction; - public function handle(Server $server, bool $force = true) + public function handle(Server $server) { - if ($force) { - instant_remote_process(['docker image prune -af'], $server, false); - instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false); - instant_remote_process(['docker builder prune -af'], $server, false); - } else { - instant_remote_process(['docker image prune -f'], $server, false); - instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false); - instant_remote_process(['docker builder prune -f'], $server, false); + $settings = instanceSettings(); + $helperImageVersion = data_get($settings, 'helper_version'); + $helperImage = config('coolify.helper_image'); + $helperImageWithVersion = "$helperImage:$helperImageVersion"; + + $commands = [ + 'docker container prune -f --filter "label=coolify.managed=true" --filter "label!=coolify.proxy=true"', + 'docker image prune -af --filter "label!=coolify.managed=true"', + 'docker builder prune -af', + "docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f", + ]; + + $serverSettings = $server->settings; + if ($serverSettings->delete_unused_volumes) { + $commands[] = 'docker volume prune -af'; + } + + if ($serverSettings->delete_unused_networks) { + $commands[] = 'docker network prune -f'; + } + + foreach ($commands as $command) { + instant_remote_process([$command], $server, false); } } } diff --git a/app/Actions/Server/ConfigureCloudflared.php b/app/Actions/Server/ConfigureCloudflared.php index 3946afe95..fc04e67a4 100644 --- a/app/Actions/Server/ConfigureCloudflared.php +++ b/app/Actions/Server/ConfigureCloudflared.php @@ -2,6 +2,7 @@ namespace App\Actions\Server; +use App\Events\CloudflareTunnelConfigured; use App\Models\Server; use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; @@ -39,9 +40,12 @@ class ConfigureCloudflared ]); instant_remote_process($commands, $server); } catch (\Throwable $e) { - ray($e); + $server->settings->is_cloudflare_tunnel = false; + $server->settings->save(); throw $e; } finally { + CloudflareTunnelConfigured::dispatch($server->team_id); + $commands = collect([ 'rm -fr /tmp/cloudflared', ]); diff --git a/app/Actions/Server/DeleteServer.php b/app/Actions/Server/DeleteServer.php new file mode 100644 index 000000000..15c892e75 --- /dev/null +++ b/app/Actions/Server/DeleteServer.php @@ -0,0 +1,17 @@ +forceDelete(); + } +} diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index f671f2d2a..ba6c23ffc 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -12,12 +12,11 @@ class InstallDocker public function handle(Server $server) { + $dockerVersion = config('constants.docker_install_version'); $supported_os_type = $server->validateOS(); if (! $supported_os_type) { throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: documentation.'); } - ray('Installing Docker on server: '.$server->name.' ('.$server->ip.')'.' with OS type: '.$supported_os_type); - $dockerVersion = '24.0'; $config = base64_encode('{ "log-driver": "json-file", "log-opts": { diff --git a/app/Actions/Server/ResourcesCheck.php b/app/Actions/Server/ResourcesCheck.php new file mode 100644 index 000000000..e6b90ba38 --- /dev/null +++ b/app/Actions/Server/ResourcesCheck.php @@ -0,0 +1,41 @@ +subSeconds($seconds))->update(['status' => 'exited']); + ServiceApplication::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + ServiceDatabase::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandalonePostgresql::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandaloneRedis::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandaloneMongodb::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandaloneMysql::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandaloneMariadb::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandaloneKeydb::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandaloneDragonfly::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandaloneClickhouse::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + } catch (\Throwable $e) { + return handleError($e); + } + } +} diff --git a/app/Actions/Server/RestartContainer.php b/app/Actions/Server/RestartContainer.php new file mode 100644 index 000000000..63361d8b7 --- /dev/null +++ b/app/Actions/Server/RestartContainer.php @@ -0,0 +1,16 @@ +restartContainer($containerName); + } +} diff --git a/app/Actions/Server/RunCommand.php b/app/Actions/Server/RunCommand.php new file mode 100644 index 000000000..254c78587 --- /dev/null +++ b/app/Actions/Server/RunCommand.php @@ -0,0 +1,17 @@ +value); + } +} diff --git a/app/Actions/Server/ServerCheck.php b/app/Actions/Server/ServerCheck.php new file mode 100644 index 000000000..1dae03fd9 --- /dev/null +++ b/app/Actions/Server/ServerCheck.php @@ -0,0 +1,269 @@ +server = $server; + try { + if ($this->server->isFunctional() === false) { + return 'Server is not functional.'; + } + + if (! $this->server->isSwarmWorker() && ! $this->server->isBuildServer()) { + + if (isset($data)) { + $data = collect($data); + + $this->server->sentinelHeartbeat(); + + $this->containers = collect(data_get($data, 'containers')); + + $filesystemUsageRoot = data_get($data, 'filesystem_usage_root.used_percentage'); + ServerStorageCheckJob::dispatch($this->server, $filesystemUsageRoot); + + $containerReplicates = null; + $this->isSentinel = true; + + } else { + ['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers(); + // ServerStorageCheckJob::dispatch($this->server); + } + + if (is_null($this->containers)) { + return 'No containers found.'; + } + + if (isset($containerReplicates)) { + foreach ($containerReplicates as $containerReplica) { + $name = data_get($containerReplica, 'Name'); + $this->containers = $this->containers->map(function ($container) use ($name, $containerReplica) { + if (data_get($container, 'Spec.Name') === $name) { + $replicas = data_get($containerReplica, 'Replicas'); + $running = str($replicas)->explode('/')[0]; + $total = str($replicas)->explode('/')[1]; + if ($running === $total) { + data_set($container, 'State.Status', 'running'); + data_set($container, 'State.Health.Status', 'healthy'); + } else { + data_set($container, 'State.Status', 'starting'); + data_set($container, 'State.Health.Status', 'unhealthy'); + } + } + + return $container; + }); + } + } + $this->checkContainers(); + + if ($this->server->isSentinelEnabled() && $this->isSentinel === false) { + CheckAndStartSentinelJob::dispatch($this->server); + } + + if ($this->server->isLogDrainEnabled()) { + $this->checkLogDrainContainer(); + } + + if ($this->server->proxySet() && ! $this->server->proxy->force_stop) { + $foundProxyContainer = $this->containers->filter(function ($value, $key) { + if ($this->server->isSwarm()) { + return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; + } else { + return data_get($value, 'Name') === '/coolify-proxy'; + } + })->first(); + if (! $foundProxyContainer) { + try { + $shouldStart = CheckProxy::run($this->server); + if ($shouldStart) { + StartProxy::run($this->server, false); + $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); + } + } catch (\Throwable $e) { + } + } else { + $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); + $this->server->save(); + $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + } + } + } + } catch (\Throwable $e) { + return handleError($e); + } + } + + private function checkLogDrainContainer() + { + $foundLogDrainContainer = $this->containers->filter(function ($value, $key) { + return data_get($value, 'Name') === '/coolify-log-drain'; + })->first(); + if ($foundLogDrainContainer) { + $status = data_get($foundLogDrainContainer, 'State.Status'); + if ($status !== 'running') { + StartLogDrain::dispatch($this->server)->onQueue('high'); + } + } else { + StartLogDrain::dispatch($this->server)->onQueue('high'); + } + } + + private function checkContainers() + { + foreach ($this->containers as $container) { + if ($this->isSentinel) { + $labels = Arr::undot(data_get($container, 'labels')); + } else { + if ($this->server->isSwarm()) { + $labels = Arr::undot(data_get($container, 'Spec.Labels')); + } else { + $labels = Arr::undot(data_get($container, 'Config.Labels')); + } + + } + $managed = data_get($labels, 'coolify.managed'); + if (! $managed) { + continue; + } + $uuid = data_get($labels, 'coolify.name'); + if (! $uuid) { + $uuid = data_get($labels, 'com.docker.compose.service'); + } + + if ($this->isSentinel) { + $containerStatus = data_get($container, 'state'); + $containerHealth = data_get($container, 'health_status'); + } else { + $containerStatus = data_get($container, 'State.Status'); + $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy'); + } + $containerStatus = "$containerStatus ($containerHealth)"; + + $applicationId = data_get($labels, 'coolify.applicationId'); + $serviceId = data_get($labels, 'coolify.serviceId'); + $databaseId = data_get($labels, 'coolify.databaseId'); + $pullRequestId = data_get($labels, 'coolify.pullRequestId'); + + if ($applicationId) { + // Application + if ($pullRequestId != 0) { + if (str($applicationId)->contains('-')) { + $applicationId = str($applicationId)->before('-'); + } + $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first(); + if ($preview) { + $preview->update(['status' => $containerStatus]); + } + } else { + $application = Application::where('id', $applicationId)->first(); + if ($application) { + $application->update([ + 'status' => $containerStatus, + 'last_online_at' => now(), + ]); + } + } + } elseif (isset($serviceId)) { + // Service + $subType = data_get($labels, 'coolify.service.subType'); + $subId = data_get($labels, 'coolify.service.subId'); + $service = Service::where('id', $serviceId)->first(); + if (! $service) { + continue; + } + if ($subType === 'application') { + $service = ServiceApplication::where('id', $subId)->first(); + } else { + $service = ServiceDatabase::where('id', $subId)->first(); + } + if ($service) { + $service->update([ + 'status' => $containerStatus, + 'last_online_at' => now(), + ]); + if ($subType === 'database') { + $isPublic = data_get($service, 'is_public'); + if ($isPublic) { + $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { + if ($this->isSentinel) { + return data_get($value, 'name') === $uuid.'-proxy'; + } else { + + if ($this->server->isSwarm()) { + return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; + } else { + return data_get($value, 'Name') === "/$uuid-proxy"; + } + } + })->first(); + if (! $foundTcpProxy) { + StartDatabaseProxy::run($service); + } + } + } + } + } else { + // Database + if (is_null($this->databases)) { + $this->databases = $this->server->databases(); + } + $database = $this->databases->where('uuid', $uuid)->first(); + if ($database) { + $database->update([ + 'status' => $containerStatus, + 'last_online_at' => now(), + ]); + + $isPublic = data_get($database, 'is_public'); + if ($isPublic) { + $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { + if ($this->isSentinel) { + return data_get($value, 'name') === $uuid.'-proxy'; + } else { + if ($this->server->isSwarm()) { + return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; + } else { + + return data_get($value, 'Name') === "/$uuid-proxy"; + } + } + })->first(); + if (! $foundTcpProxy) { + StartDatabaseProxy::run($database); + // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); + } + } + } + } + } + } +} diff --git a/app/Actions/Server/InstallLogDrain.php b/app/Actions/Server/StartLogDrain.php similarity index 87% rename from app/Actions/Server/InstallLogDrain.php rename to app/Actions/Server/StartLogDrain.php index 6f74e020b..0e8036cd9 100644 --- a/app/Actions/Server/InstallLogDrain.php +++ b/app/Actions/Server/StartLogDrain.php @@ -5,7 +5,7 @@ namespace App\Actions\Server; use App\Models\Server; use Lorisleiva\Actions\Concerns\AsAction; -class InstallLogDrain +class StartLogDrain { use AsAction; @@ -13,23 +13,22 @@ class InstallLogDrain { if ($server->settings->is_logdrain_newrelic_enabled) { $type = 'newrelic'; + StopLogDrain::run($server); } elseif ($server->settings->is_logdrain_highlight_enabled) { $type = 'highlight'; + StopLogDrain::run($server); } elseif ($server->settings->is_logdrain_axiom_enabled) { $type = 'axiom'; + StopLogDrain::run($server); } elseif ($server->settings->is_logdrain_custom_enabled) { $type = 'custom'; + StopLogDrain::run($server); } else { $type = 'none'; } try { if ($type === 'none') { - $command = [ - "echo 'Stopping old Fluent Bit'", - 'docker rm -f coolify-log-drain || true', - ]; - - return instant_remote_process($command, $server); + return 'No log drain is enabled.'; } elseif ($type === 'newrelic') { if (! $server->settings->is_logdrain_newrelic_enabled) { throw new \Exception('New Relic log drain is not enabled.'); @@ -52,7 +51,11 @@ class InstallLogDrain [FILTER] Name modify Match * - Set server_name {$server->name} + Set coolify.server_name {$server->name} + Rename COOLIFY_APP_NAME coolify.app_name + Rename COOLIFY_PROJECT_NAME coolify.project_name + Rename COOLIFY_SERVER_IP coolify.server_ip + Rename COOLIFY_ENVIRONMENT_NAME coolify.environment_name [OUTPUT] Name nrlogs Match * @@ -103,7 +106,11 @@ class InstallLogDrain [FILTER] Name modify Match * - Set server_name {$server->name} + Set coolify.server_name {$server->name} + Rename COOLIFY_APP_NAME coolify.app_name + Rename COOLIFY_PROJECT_NAME coolify.project_name + Rename COOLIFY_SERVER_IP coolify.server_ip + Rename COOLIFY_ENVIRONMENT_NAME coolify.environment_name [OUTPUT] Name http Match * @@ -148,6 +155,8 @@ services: - ./parsers.conf:/parsers.conf ports: - 127.0.0.1:24224:24224 + labels: + - coolify.managed=true restart: unless-stopped '); $readme = base64_encode('# New Relic Log Drain @@ -199,10 +208,8 @@ Files: throw new \Exception('Unknown log drain type.'); } $restart_command = [ - "echo 'Stopping old Fluent Bit'", - "cd $config_path && docker compose down --remove-orphans || true", "echo 'Starting Fluent Bit'", - "cd $config_path && docker compose up -d --remove-orphans", + "cd $config_path && docker compose up -d", ]; $command = array_merge($command, $add_envs_command, $restart_command); diff --git a/app/Actions/Server/StartSentinel.php b/app/Actions/Server/StartSentinel.php index eea429c79..587ac4a8d 100644 --- a/app/Actions/Server/StartSentinel.php +++ b/app/Actions/Server/StartSentinel.php @@ -9,15 +9,57 @@ class StartSentinel { use AsAction; - public function handle(Server $server, $version = 'latest', bool $restart = false) + public function handle(Server $server, bool $restart = false, ?string $latestVersion = null) { - if ($restart) { - instant_remote_process(['docker rm -f coolify-sentinel'], $server, false); + if ($server->isSwarm() || $server->isBuildServer()) { + return; } + if ($restart) { + StopSentinel::run($server); + } + $version = $latestVersion ?? get_latest_sentinel_version(); + $metricsHistory = data_get($server, 'settings.sentinel_metrics_history_days'); + $refreshRate = data_get($server, 'settings.sentinel_metrics_refresh_rate_seconds'); + $pushInterval = data_get($server, 'settings.sentinel_push_interval_seconds'); + $token = data_get($server, 'settings.sentinel_token'); + $endpoint = data_get($server, 'settings.sentinel_custom_url'); + $debug = data_get($server, 'settings.is_sentinel_debug_enabled'); + $mountDir = '/data/coolify/sentinel'; + $image = "ghcr.io/coollabsio/sentinel:$version"; + if (! $endpoint) { + throw new \Exception('You should set FQDN in Instance Settings.'); + } + $environments = [ + 'TOKEN' => $token, + 'DEBUG' => $debug ? 'true' : 'false', + 'PUSH_ENDPOINT' => $endpoint, + 'PUSH_INTERVAL_SECONDS' => $pushInterval, + 'COLLECTOR_ENABLED' => $server->isMetricsEnabled() ? 'true' : 'false', + 'COLLECTOR_REFRESH_RATE_SECONDS' => $refreshRate, + 'COLLECTOR_RETENTION_PERIOD_DAYS' => $metricsHistory, + ]; + $labels = [ + 'coolify.managed' => 'true', + ]; + if (isDev()) { + // data_set($environments, 'DEBUG', 'true'); + // $image = 'sentinel'; + $mountDir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/sentinel'; + } + $dockerEnvironments = '-e "'.implode('" -e "', array_map(fn ($key, $value) => "$key=$value", array_keys($environments), $environments)).'"'; + $dockerLabels = implode(' ', array_map(fn ($key, $value) => "$key=$value", array_keys($labels), $labels)); + $dockerCommand = "docker run -d $dockerEnvironments --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v $mountDir:/app/db --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 --add-host=host.docker.internal:host-gateway --label $dockerLabels $image"; + instant_remote_process([ - "docker run --rm --pull always -d -e \"SCHEDULER=true\" --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify/metrics:/app/metrics -v /data/coolify/logs:/app/logs --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 ghcr.io/coollabsio/sentinel:$version", - 'chown -R 9999:root /data/coolify/metrics /data/coolify/logs', - 'chmod -R 700 /data/coolify/metrics /data/coolify/logs', - ], $server, false); + 'docker rm -f coolify-sentinel || true', + "mkdir -p $mountDir", + $dockerCommand, + "chown -R 9999:root $mountDir", + "chmod -R 700 $mountDir", + ], $server); + + $server->settings->is_sentinel_enabled = true; + $server->settings->save(); + $server->sentinelHeartbeat(); } } diff --git a/app/Actions/Server/StopLogDrain.php b/app/Actions/Server/StopLogDrain.php new file mode 100644 index 000000000..96c2466de --- /dev/null +++ b/app/Actions/Server/StopLogDrain.php @@ -0,0 +1,20 @@ +sentinelHeartbeat(isReset: true); + } +} diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php index 0e97d2aed..d57a4fe46 100644 --- a/app/Actions/Server/UpdateCoolify.php +++ b/app/Actions/Server/UpdateCoolify.php @@ -2,8 +2,9 @@ namespace App\Actions\Server; -use App\Models\InstanceSettings; +use App\Jobs\PullHelperImageJob; use App\Models\Server; +use Illuminate\Support\Sleep; use Lorisleiva\Actions\Concerns\AsAction; class UpdateCoolify @@ -18,46 +19,44 @@ class UpdateCoolify public function handle($manual_update = false) { - try { - $settings = InstanceSettings::get(); - ray('Running InstanceAutoUpdateJob'); - $this->server = Server::find(0); - if (! $this->server) { + if (isDev()) { + Sleep::for(10)->seconds(); + + return; + } + $settings = instanceSettings(); + $this->server = Server::find(0); + if (! $this->server) { + return; + } + CleanupDocker::dispatch($this->server)->onQueue('high'); + $this->latestVersion = get_latest_version_of_coolify(); + $this->currentVersion = config('version'); + if (! $manual_update) { + if (! $settings->is_auto_update_enabled) { return; } - CleanupDocker::run($this->server, false); - $this->latestVersion = get_latest_version_of_coolify(); - $this->currentVersion = config('version'); - if (! $manual_update) { - if (! $settings->is_auto_update_enabled) { - return; - } - if ($this->latestVersion === $this->currentVersion) { - return; - } - if (version_compare($this->latestVersion, $this->currentVersion, '<')) { - return; - } + if ($this->latestVersion === $this->currentVersion) { + return; + } + if (version_compare($this->latestVersion, $this->currentVersion, '<')) { + return; } - $this->update(); - } catch (\Throwable $e) { - throw $e; } + $this->update(); + $settings->new_version_available = false; + $settings->save(); } private function update() { - if (isDev()) { - remote_process([ - 'sleep 10', - ], $this->server); + PullHelperImageJob::dispatch($this->server); + + instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false); - return; - } remote_process([ 'curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh', "bash /data/coolify/source/upgrade.sh $this->latestVersion", ], $this->server); - } } diff --git a/app/Actions/Server/ValidateServer.php b/app/Actions/Server/ValidateServer.php new file mode 100644 index 000000000..d0a4cd6be --- /dev/null +++ b/app/Actions/Server/ValidateServer.php @@ -0,0 +1,67 @@ +update([ + 'validation_logs' => null, + ]); + ['uptime' => $this->uptime, 'error' => $error] = $server->validateConnection(); + if (! $this->uptime) { + $this->error = 'Server is not reachable. Please validate your configuration and connection.
Check this documentation for further help.

Error: '.$error.'
'; + $server->update([ + 'validation_logs' => $this->error, + ]); + throw new \Exception($this->error); + } + $this->supported_os_type = $server->validateOS(); + if (! $this->supported_os_type) { + $this->error = 'Server OS type is not supported. Please install Docker manually before continuing: documentation.'; + $server->update([ + 'validation_logs' => $this->error, + ]); + throw new \Exception($this->error); + } + + $this->docker_installed = $server->validateDockerEngine(); + $this->docker_compose_installed = $server->validateDockerCompose(); + if (! $this->docker_installed || ! $this->docker_compose_installed) { + $this->error = 'Docker Engine is not installed. Please install Docker manually before continuing: documentation.'; + $server->update([ + 'validation_logs' => $this->error, + ]); + throw new \Exception($this->error); + } + $this->docker_version = $server->validateDockerEngineVersion(); + + if ($this->docker_version) { + return 'OK'; + } else { + $this->error = 'Docker Engine is not installed. Please install Docker manually before continuing: documentation.'; + $server->update([ + 'validation_logs' => $this->error, + ]); + throw new \Exception($this->error); + } + } +} diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php index 194cf4db9..9b87454da 100644 --- a/app/Actions/Service/DeleteService.php +++ b/app/Actions/Service/DeleteService.php @@ -2,18 +2,20 @@ namespace App\Actions\Service; +use App\Actions\Server\CleanupDocker; use App\Models\Service; +use Illuminate\Support\Facades\Log; use Lorisleiva\Actions\Concerns\AsAction; class DeleteService { use AsAction; - public function handle(Service $service) + public function handle(Service $service, bool $deleteConfigurations, bool $deleteVolumes, bool $dockerCleanup, bool $deleteConnectedNetworks) { try { $server = data_get($service, 'server'); - if ($server->isFunctional()) { + if ($deleteVolumes && $server->isFunctional()) { $storagesToDelete = collect([]); $service->environment_variables()->delete(); @@ -33,13 +35,29 @@ class DeleteService foreach ($storagesToDelete as $storage) { $commands[] = "docker volume rm -f $storage->name"; } - $commands[] = "docker rm -f $service->uuid"; - instant_remote_process($commands, $server, false); + // Execute volume deletion first, this must be done first otherwise volumes will not be deleted. + if (! empty($commands)) { + foreach ($commands as $command) { + $result = instant_remote_process([$command], $server, false); + if ($result !== null && $result !== 0) { + Log::error('Error deleting volumes: '.$result); + } + } + } } + + if ($deleteConnectedNetworks) { + $service->delete_connected_networks($service->uuid); + } + + instant_remote_process(["docker rm -f $service->uuid"], $server, throwError: false); } catch (\Exception $e) { throw new \Exception($e->getMessage()); } finally { + if ($deleteConfigurations) { + $service->delete_configurations(); + } foreach ($service->applications()->get() as $application) { $application->forceDelete(); } @@ -50,6 +68,11 @@ class DeleteService $task->delete(); } $service->tags()->detach(); + $service->forceDelete(); + + if ($dockerCleanup) { + CleanupDocker::dispatch($server, true); + } } } } diff --git a/app/Actions/Service/RestartService.php b/app/Actions/Service/RestartService.php new file mode 100644 index 000000000..1b6a5c32c --- /dev/null +++ b/app/Actions/Service/RestartService.php @@ -0,0 +1,18 @@ +name); $service->saveComposeConfigs(); $commands[] = 'cd '.$service->workdir(); $commands[] = "echo 'Saved configuration files to {$service->workdir()}.'"; - $commands[] = "echo 'Creating Docker network.'"; - $commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid"; + if ($service->networks()->count() > 0) { + $commands[] = "echo 'Creating Docker network.'"; + $commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid"; + } $commands[] = 'echo Starting service.'; $commands[] = "echo 'Pulling images.'"; $commands[] = 'docker compose pull'; @@ -29,11 +30,10 @@ class StartService $network = $service->destination->network; $serviceNames = data_get(Yaml::parse($compose), 'services', []); foreach ($serviceNames as $serviceName => $serviceConfig) { - $commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} || true"; + $commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} >/dev/null 2>&1 || true"; } } - $activity = remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged'); - return $activity; + return remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged'); } } diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index 4c0042ebd..046d94ced 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -2,6 +2,7 @@ namespace App\Actions\Service; +use App\Actions\Server\CleanupDocker; use App\Models\Service; use Lorisleiva\Actions\Concerns\AsAction; @@ -9,34 +10,25 @@ class StopService { use AsAction; - public function handle(Service $service) + public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true) { try { $server = $service->destination->server; if (! $server->isFunctional()) { return 'Server is not functional'; } - ray('Stopping service: '.$service->name); - $applications = $service->applications()->get(); - foreach ($applications as $application) { - instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server); - $application->update(['status' => 'exited']); - } - $dbs = $service->databases()->get(); - foreach ($dbs as $db) { - instant_remote_process(["docker rm -f {$db->name}-{$service->uuid}"], $service->server); - $db->update(['status' => 'exited']); - } - instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy 2>/dev/null"], $service->server, false); - instant_remote_process(["docker network rm {$service->uuid} 2>/dev/null"], $service->server, false); - // TODO: make notification for databases - // $service->environment->project->team->notify(new StatusChanged($service)); - } catch (\Exception $e) { - echo $e->getMessage(); - ray($e->getMessage()); + $containersToStop = $service->getContainersToStop(); + $service->stopContainers($containersToStop, $server); + + if (! $isDeleteOperation) { + $service->delete_connected_networks($service->uuid); + if ($dockerCleanup) { + CleanupDocker::dispatch($server, true); + } + } + } catch (\Exception $e) { return $e->getMessage(); } - } } diff --git a/app/Console/Commands/CheckApplicationDeploymentQueue.php b/app/Console/Commands/CheckApplicationDeploymentQueue.php new file mode 100644 index 000000000..e89d26f2c --- /dev/null +++ b/app/Console/Commands/CheckApplicationDeploymentQueue.php @@ -0,0 +1,50 @@ +option('seconds'); + $deployments = ApplicationDeploymentQueue::whereIn('status', [ + ApplicationDeploymentStatus::IN_PROGRESS, + ApplicationDeploymentStatus::QUEUED, + ])->where('created_at', '<=', now()->subSeconds($seconds))->get(); + if ($deployments->isEmpty()) { + $this->info('No deployments found in the last '.$seconds.' seconds.'); + + return; + } + + $this->info('Found '.$deployments->count().' deployments created in the last '.$seconds.' seconds.'); + + foreach ($deployments as $deployment) { + if ($this->option('force')) { + $this->info('Deployment '.$deployment->id.' created at '.$deployment->created_at.' is older than '.$seconds.' seconds. Setting status to failed.'); + $this->cancelDeployment($deployment); + } else { + $this->info('Deployment '.$deployment->id.' created at '.$deployment->created_at.' is older than '.$seconds.' seconds. Setting status to failed.'); + if ($this->confirm('Do you want to cancel this deployment?', true)) { + $this->cancelDeployment($deployment); + } + } + } + } + + private function cancelDeployment(ApplicationDeploymentQueue $deployment) + { + $deployment->update(['status' => ApplicationDeploymentStatus::FAILED]); + if ($deployment->server?->isFunctional()) { + remote_process(['docker rm -f '.$deployment->deployment_uuid], $deployment->server, false); + } + } +} diff --git a/app/Console/Commands/CleanupApplicationDeploymentQueue.php b/app/Console/Commands/CleanupApplicationDeploymentQueue.php index f068e3eb2..3aae28ae6 100644 --- a/app/Console/Commands/CleanupApplicationDeploymentQueue.php +++ b/app/Console/Commands/CleanupApplicationDeploymentQueue.php @@ -7,9 +7,9 @@ use Illuminate\Console\Command; class CleanupApplicationDeploymentQueue extends Command { - protected $signature = 'cleanup:application-deployment-queue {--team-id=}'; + protected $signature = 'cleanup:deployment-queue {--team-id=}'; - protected $description = 'CleanupApplicationDeploymentQueue'; + protected $description = 'Cleanup application deployment queue.'; public function handle() { diff --git a/app/Console/Commands/CleanupDatabase.php b/app/Console/Commands/CleanupDatabase.php index 1e177ca62..a0adc8b36 100644 --- a/app/Console/Commands/CleanupDatabase.php +++ b/app/Console/Commands/CleanupDatabase.php @@ -7,7 +7,7 @@ use Illuminate\Support\Facades\DB; class CleanupDatabase extends Command { - protected $signature = 'cleanup:database {--yes}'; + protected $signature = 'cleanup:database {--yes} {--keep-days=}'; protected $description = 'Cleanup database'; @@ -18,7 +18,12 @@ class CleanupDatabase extends Command } else { echo "Running database cleanup in dry-run mode...\n"; } - $keep_days = 60; + if (isCloud()) { + // Later on we can increase this to 180 days or dynamically set + $keep_days = $this->option('keep-days') ?? 60; + } else { + $keep_days = $this->option('keep-days') ?? 60; + } echo "Keep days: $keep_days\n"; // Cleanup failed jobs table $failed_jobs = DB::table('failed_jobs')->where('failed_at', '<', now()->subDays(1)); @@ -59,6 +64,5 @@ class CleanupDatabase extends Command if ($this->option('yes')) { $webhooks->delete(); } - } } diff --git a/app/Console/Commands/CleanupQueue.php b/app/Console/Commands/CleanupQueue.php deleted file mode 100644 index fd2b637ac..000000000 --- a/app/Console/Commands/CleanupQueue.php +++ /dev/null @@ -1,24 +0,0 @@ -keys('*:laravel*'); - foreach ($keys as $key) { - $keyWithoutPrefix = str_replace($prefix, '', $key); - Redis::connection()->del($keyWithoutPrefix); - } - } -} diff --git a/app/Console/Commands/CleanupRedis.php b/app/Console/Commands/CleanupRedis.php new file mode 100644 index 000000000..5fc2b4e61 --- /dev/null +++ b/app/Console/Commands/CleanupRedis.php @@ -0,0 +1,30 @@ +keys('*:laravel*'); + collect($keys)->each(function ($key) use ($prefix) { + $keyWithoutPrefix = str_replace($prefix, '', $key); + Redis::connection()->del($keyWithoutPrefix); + }); + + $queueOverlaps = Redis::connection()->keys('*laravel-queue-overlap*'); + collect($queueOverlaps)->each(function ($key) { + Redis::connection()->del($key); + }); + } +} diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php index fbbf2c820..9d36ce9b8 100644 --- a/app/Console/Commands/CleanupStuckedResources.php +++ b/app/Console/Commands/CleanupStuckedResources.php @@ -2,8 +2,13 @@ namespace App\Console\Commands; +use App\Jobs\CleanupHelperContainersJob; use App\Models\Application; +use App\Models\ApplicationDeploymentQueue; +use App\Models\ApplicationPreview; +use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledTask; +use App\Models\Server; use App\Models\Service; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; @@ -25,14 +30,33 @@ class CleanupStuckedResources extends Command public function handle() { - ray('Running cleanup stucked resources.'); echo "Running cleanup stucked resources.\n"; $this->cleanup_stucked_resources(); } private function cleanup_stucked_resources() { - + try { + $servers = Server::all()->filter(function ($server) { + return $server->isFunctional(); + }); + foreach ($servers as $server) { + CleanupHelperContainersJob::dispatch($server); + } + } catch (\Throwable $e) { + echo "Error in cleaning stucked resources: {$e->getMessage()}\n"; + } + try { + $applicationsDeploymentQueue = ApplicationDeploymentQueue::get(); + foreach ($applicationsDeploymentQueue as $applicationDeploymentQueue) { + if (is_null($applicationDeploymentQueue->application)) { + echo "Deleting stuck application deployment queue: {$applicationDeploymentQueue->id}\n"; + $applicationDeploymentQueue->delete(); + } + } + } catch (\Throwable $e) { + echo "Error in cleaning stuck application deployment queue: {$e->getMessage()}\n"; + } try { $applications = Application::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($applications as $application) { @@ -42,6 +66,17 @@ class CleanupStuckedResources extends Command } catch (\Throwable $e) { echo "Error in cleaning stuck application: {$e->getMessage()}\n"; } + try { + $applicationsPreviews = ApplicationPreview::get(); + foreach ($applicationsPreviews as $applicationPreview) { + if (! data_get($applicationPreview, 'application')) { + echo "Deleting stuck application preview: {$applicationPreview->uuid}\n"; + $applicationPreview->delete(); + } + } + } catch (\Throwable $e) { + echo "Error in cleaning stuck application: {$e->getMessage()}\n"; + } try { $postgresqls = StandalonePostgresql::withTrashed()->whereNotNull('deleted_at')->get(); foreach ($postgresqls as $postgresql) { @@ -153,6 +188,18 @@ class CleanupStuckedResources extends Command echo "Error in cleaning stuck scheduledtasks: {$e->getMessage()}\n"; } + try { + $scheduled_backups = ScheduledDatabaseBackup::all(); + foreach ($scheduled_backups as $scheduled_backup) { + if (! $scheduled_backup->server()) { + echo "Deleting stuck scheduledbackup: {$scheduled_backup->name}\n"; + $scheduled_backup->delete(); + } + } + } catch (\Throwable $e) { + echo "Error in cleaning stuck scheduledbackups: {$e->getMessage()}\n"; + } + // Cleanup any resources that are not attached to any environment or destination or server try { $applications = Application::all(); diff --git a/app/Console/Commands/CleanupUnreachableServers.php b/app/Console/Commands/CleanupUnreachableServers.php index 328628039..df0c6b81b 100644 --- a/app/Console/Commands/CleanupUnreachableServers.php +++ b/app/Console/Commands/CleanupUnreachableServers.php @@ -18,7 +18,7 @@ class CleanupUnreachableServers extends Command if ($servers->count() > 0) { foreach ($servers as $server) { echo "Cleanup unreachable server ($server->id) with name $server->name"; - send_internal_notification("Server $server->name is unreachable for 7 days. Cleaning up..."); + // send_internal_notification("Server $server->name is unreachable for 7 days. Cleaning up..."); $server->update([ 'ip' => '1.2.3.4', ]); diff --git a/app/Console/Commands/CloudCheckSubscription.php b/app/Console/Commands/CloudCheckSubscription.php new file mode 100644 index 000000000..6e237e84b --- /dev/null +++ b/app/Console/Commands/CloudCheckSubscription.php @@ -0,0 +1,49 @@ +get(); + foreach ($activeSubscribers as $team) { + $stripeSubscriptionId = $team->subscription->stripe_subscription_id; + $stripeInvoicePaid = $team->subscription->stripe_invoice_paid; + $stripeCustomerId = $team->subscription->stripe_customer_id; + if (! $stripeSubscriptionId) { + echo "Team {$team->id} has no subscription, but invoice status is: {$stripeInvoicePaid}\n"; + echo "Link on Stripe: https://dashboard.stripe.com/customers/{$stripeCustomerId}\n"; + + continue; + } + $subscription = $stripe->subscriptions->retrieve($stripeSubscriptionId); + if ($subscription->status === 'active') { + continue; + } + echo "Subscription {$stripeSubscriptionId} is not active ({$subscription->status})\n"; + echo "Link on Stripe: https://dashboard.stripe.com/subscriptions/{$stripeSubscriptionId}\n"; + } + } +} diff --git a/app/Console/Commands/CloudCleanupSubscriptions.php b/app/Console/Commands/CloudCleanupSubscriptions.php new file mode 100644 index 000000000..8bb420ab8 --- /dev/null +++ b/app/Console/Commands/CloudCleanupSubscriptions.php @@ -0,0 +1,98 @@ +error('This command can only be run on cloud'); + + return; + } + $this->info('Cleaning up subcriptions teams'); + $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); + + $teams = Team::all()->filter(function ($team) { + return $team->id !== 0; + })->sortBy('id'); + foreach ($teams as $team) { + if ($team) { + $this->info("Checking team {$team->id}"); + } + if (! data_get($team, 'subscription')) { + $this->disableServers($team); + + continue; + } + // If the team has no subscription id and the invoice is paid, we need to reset the invoice paid status + if (! (data_get($team, 'subscription.stripe_subscription_id'))) { + $this->info("Resetting invoice paid status for team {$team->id} {$team->name}"); + + $team->subscription->update([ + 'stripe_invoice_paid' => false, + 'stripe_trial_already_ended' => false, + 'stripe_subscription_id' => null, + ]); + $this->disableServers($team); + + continue; + } else { + $subscription = $stripe->subscriptions->retrieve(data_get($team, 'subscription.stripe_subscription_id'), []); + $status = data_get($subscription, 'status'); + if ($status === 'active' || $status === 'past_due') { + $team->subscription->update([ + 'stripe_invoice_paid' => true, + 'stripe_trial_already_ended' => false, + ]); + + continue; + } + $this->info('Subscription status: '.$status); + $this->info('Subscription id: '.data_get($team, 'subscription.stripe_subscription_id')); + $confirm = $this->confirm('Do you want to cancel the subscription?', true); + if (! $confirm) { + $this->info("Skipping team {$team->id} {$team->name}"); + } else { + $this->info("Cancelling subscription for team {$team->id} {$team->name}"); + $team->subscription->update([ + 'stripe_invoice_paid' => false, + 'stripe_trial_already_ended' => false, + 'stripe_subscription_id' => null, + ]); + $this->disableServers($team); + } + } + } + } catch (\Exception $e) { + $this->error($e->getMessage()); + + return; + } + } + + private function disableServers(Team $team) + { + foreach ($team->servers as $server) { + if ($server->settings->is_usable === true || $server->settings->is_reachable === true || $server->ip !== '1.2.3.4') { + $this->info("Disabling server {$server->id} {$server->name}"); + $server->settings()->update([ + 'is_usable' => false, + 'is_reachable' => false, + ]); + $server->update([ + 'ip' => '1.2.3.4', + ]); + } + } + } +} diff --git a/app/Console/Commands/Dev.php b/app/Console/Commands/Dev.php index 80059bf00..f5f1233fe 100644 --- a/app/Console/Commands/Dev.php +++ b/app/Console/Commands/Dev.php @@ -9,17 +9,51 @@ use Illuminate\Support\Facades\Process; class Dev extends Command { - protected $signature = 'dev:init'; + protected $signature = 'dev {--init} {--generate-openapi}'; - protected $description = 'Init the app in dev mode'; + protected $description = 'Helper commands for development.'; public function handle() + { + if ($this->option('init')) { + $this->init(); + + return; + } + if ($this->option('generate-openapi')) { + $this->generateOpenApi(); + + return; + } + } + + public function generateOpenApi() + { + // Generate OpenAPI documentation + echo "Generating OpenAPI documentation.\n"; + $process = Process::run(['/var/www/html/vendor/bin/openapi', 'app', '-o', 'openapi.yaml']); + $error = $process->errorOutput(); + $error = preg_replace('/^.*an object literal,.*$/m', '', $error); + $error = preg_replace('/^\h*\v+/m', '', $error); + echo $error; + echo $process->output(); + } + + public function init() { // Generate APP_KEY if not exists + if (empty(env('APP_KEY'))) { echo "Generating APP_KEY.\n"; Artisan::call('key:generate'); } + + // Generate STORAGE link if not exists + if (! file_exists(public_path('storage'))) { + echo "Generating STORAGE link.\n"; + Artisan::call('storage:link'); + } + // Seed database if it's empty $settings = InstanceSettings::find(0); if (! $settings) { diff --git a/app/Console/Commands/Emails.php b/app/Console/Commands/Emails.php index 8ad0d458f..cda4ca84f 100644 --- a/app/Console/Commands/Emails.php +++ b/app/Console/Commands/Emails.php @@ -15,7 +15,6 @@ use App\Notifications\Application\DeploymentSuccess; use App\Notifications\Application\StatusChanged; use App\Notifications\Database\BackupFailed; use App\Notifications\Database\BackupSuccess; -use App\Notifications\Database\DailyBackup; use App\Notifications\Test; use Exception; use Illuminate\Console\Command; @@ -81,7 +80,7 @@ class Emails extends Command } set_transanctional_email_settings(); - $this->mail = new MailMessage(); + $this->mail = new MailMessage; $this->mail->subject('Test Email'); switch ($type) { case 'updates': @@ -107,7 +106,7 @@ class Emails extends Command $confirmed = confirm('Are you sure?'); if ($confirmed) { foreach ($emails as $email) { - $this->mail = new MailMessage(); + $this->mail = new MailMessage; $this->mail->subject('One-click Services, Docker Compose support'); $unsubscribeUrl = route('unsubscribe.marketing.emails', [ 'token' => encrypt($email), @@ -118,31 +117,13 @@ class Emails extends Command } break; case 'emails-test': - $this->mail = (new Test())->toMail(); - $this->sendEmail(); - break; - case 'database-backup-statuses-daily': - $scheduled_backups = ScheduledDatabaseBackup::all(); - $databases = collect(); - foreach ($scheduled_backups as $scheduled_backup) { - $last_days_backups = $scheduled_backup->get_last_days_backup_status(); - if ($last_days_backups->isEmpty()) { - continue; - } - $failed = $last_days_backups->where('status', 'failed'); - $database = $scheduled_backup->database; - $databases->put($database->name, [ - 'failed_count' => $failed->count(), - ]); - } - $this->mail = (new DailyBackup($databases))->toMail(); + $this->mail = (new Test)->toMail(); $this->sendEmail(); break; case 'application-deployment-success-daily': $applications = Application::all(); foreach ($applications as $application) { $deployments = $application->get_last_days_deployments(); - ray($deployments); if ($deployments->isEmpty()) { continue; } @@ -224,7 +205,7 @@ class Emails extends Command // $this->sendEmail(); // break; case 'waitlist-invitation-link': - $this->mail = new MailMessage(); + $this->mail = new MailMessage; $this->mail->view('emails.waitlist-invitation', [ 'loginLink' => 'https://coolify.io', ]); @@ -241,7 +222,7 @@ class Emails extends Command break; case 'realusers-before-trial': - $this->mail = new MailMessage(); + $this->mail = new MailMessage; $this->mail->view('emails.before-trial-conversion'); $this->mail->subject('Trial period has been added for all subscription plans.'); $teams = Team::doesntHave('subscription')->where('id', '!=', 0)->get(); @@ -287,7 +268,7 @@ class Emails extends Command foreach ($admins as $admin) { $this->info($admin); } - $this->mail = new MailMessage(); + $this->mail = new MailMessage; $this->mail->view('emails.server-lost-connection', [ 'name' => $server->name, ]); diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index 50c9fe29b..c802fb116 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -2,51 +2,74 @@ namespace App\Console\Commands; +use App\Actions\Server\StopSentinel; +use App\Enums\ActivityTypes; use App\Enums\ApplicationDeploymentStatus; -use App\Jobs\CleanupHelperContainersJob; use App\Models\ApplicationDeploymentQueue; -use App\Models\InstanceSettings; +use App\Models\Environment; use App\Models\ScheduledDatabaseBackup; use App\Models\Server; use App\Models\StandalonePostgresql; +use App\Models\User; use Illuminate\Console\Command; +use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Http; class Init extends Command { - protected $signature = 'app:init {--full-cleanup} {--cleanup-deployments}'; + protected $signature = 'app:init {--force-cloud}'; protected $description = 'Cleanup instance related stuffs'; + public $servers = null; + public function handle() { - $this->alive(); - get_public_ips(); - $full_cleanup = $this->option('full-cleanup'); - $cleanup_deployments = $this->option('cleanup-deployments'); - if ($cleanup_deployments) { - echo "Running cleanup deployments.\n"; - $this->cleanup_in_progress_application_deployments(); + if (isCloud() && ! $this->option('force-cloud')) { + echo "Skipping init as we are on cloud and --force-cloud option is not set\n"; return; } - if ($full_cleanup) { - // Required for falsely deleted coolify db - $this->restore_coolify_db_backup(); - $this->cleanup_in_progress_application_deployments(); - $this->cleanup_stucked_helper_containers(); - $this->call('cleanup:queue'); - $this->call('cleanup:stucked-resources'); - if (! isCloud()) { - try { - $server = Server::find(0)->first(); - $server->setupDynamicProxyConfiguration(); - } catch (\Throwable $e) { - echo "Could not setup dynamic configuration: {$e->getMessage()}\n"; - } - } - $settings = InstanceSettings::get(); + $this->servers = Server::all(); + if (isCloud()) { + } else { + $this->send_alive_signal(); + get_public_ips(); + } + + // Backward compatibility + // $this->disable_metrics(); + $this->replace_slash_in_environment_name(); + $this->restore_coolify_db_backup(); + $this->update_user_emails(); + // + $this->update_traefik_labels(); + if (! isCloud() || $this->option('force-cloud')) { + $this->cleanup_unused_network_from_coolify_proxy(); + } + if (isCloud()) { + $this->cleanup_unnecessary_dynamic_proxy_configuration(); + } else { + $this->cleanup_in_progress_application_deployments(); + } + $this->call('cleanup:redis'); + $this->call('cleanup:stucked-resources'); + + if (isCloud()) { + $response = Http::retry(3, 1000)->get(config('constants.services.official')); + if ($response->successful()) { + $services = $response->json(); + File::put(base_path('templates/service-templates.json'), json_encode($services)); + } + } else { + try { + $localhost = $this->servers->where('id', 0)->first(); + $localhost->setupDynamicProxyConfiguration(); + } catch (\Throwable $e) { + echo "Could not setup dynamic configuration: {$e->getMessage()}\n"; + } + $settings = instanceSettings(); if (! is_null(env('AUTOUPDATE', null))) { if (env('AUTOUPDATE') == true) { $settings->update(['is_auto_update_enabled' => true]); @@ -54,53 +77,134 @@ class Init extends Command $settings->update(['is_auto_update_enabled' => false]); } } - - return; } - $this->cleanup_stucked_helper_containers(); - $this->call('cleanup:stucked-resources'); + } + + // private function disable_metrics() + // { + // if (version_compare('4.0.0-beta.312', config('version'), '<=')) { + // foreach ($this->servers as $server) { + // if ($server->settings->is_metrics_enabled === true) { + // $server->settings->update(['is_metrics_enabled' => false]); + // } + // if ($server->isFunctional()) { + // StopSentinel::dispatch($server)->onQueue('high'); + // } + // } + // } + // } + + private function update_user_emails() + { + try { + User::whereRaw('email ~ \'[A-Z]\'')->get()->each(fn (User $user) => $user->update(['email' => strtolower($user->email)])); + } catch (\Throwable $e) { + echo "Error in updating user emails: {$e->getMessage()}\n"; + } + } + + private function update_traefik_labels() + { + try { + Server::where('proxy->type', 'TRAEFIK_V2')->update(['proxy->type' => 'TRAEFIK']); + } catch (\Throwable $e) { + echo "Error in updating traefik labels: {$e->getMessage()}\n"; + } + } + + private function cleanup_unnecessary_dynamic_proxy_configuration() + { + foreach ($this->servers as $server) { + try { + if (! $server->isFunctional()) { + continue; + } + if ($server->id === 0) { + continue; + } + $file = $server->proxyPath().'/dynamic/coolify.yaml'; + + return instant_remote_process([ + "rm -f $file", + ], $server, false); + } catch (\Throwable $e) { + echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n"; + } + } + } + + private function cleanup_unused_network_from_coolify_proxy() + { + foreach ($this->servers as $server) { + if (! $server->isFunctional()) { + continue; + } + if (! $server->isProxyShouldRun()) { + continue; + } + try { + ['networks' => $networks, 'allNetworks' => $allNetworks] = collectDockerNetworksByServer($server); + $removeNetworks = $allNetworks->diff($networks); + $commands = collect(); + foreach ($removeNetworks as $network) { + $out = instant_remote_process(["docker network inspect -f json $network | jq '.[].Containers | if . == {} then null else . end'"], $server, false); + if (empty($out)) { + $commands->push("docker network disconnect $network coolify-proxy >/dev/null 2>&1 || true"); + $commands->push("docker network rm $network >/dev/null 2>&1 || true"); + } else { + $data = collect(json_decode($out, true)); + if ($data->count() === 1) { + // If only coolify-proxy itself is connected to that network (it should not be possible, but who knows) + $isCoolifyProxyItself = data_get($data->first(), 'Name') === 'coolify-proxy'; + if ($isCoolifyProxyItself) { + $commands->push("docker network disconnect $network coolify-proxy >/dev/null 2>&1 || true"); + $commands->push("docker network rm $network >/dev/null 2>&1 || true"); + } + } + } + } + if ($commands->isNotEmpty()) { + echo "Cleaning up unused networks from coolify proxy\n"; + remote_process(command: $commands, type: ActivityTypes::INLINE->value, server: $server, ignore_errors: false); + } + } catch (\Throwable $e) { + echo "Error in cleaning up unused networks from coolify proxy: {$e->getMessage()}\n"; + } + } } private function restore_coolify_db_backup() { - try { - $database = StandalonePostgresql::withTrashed()->find(0); - if ($database && $database->trashed()) { - echo "Restoring coolify db backup\n"; - $database->restore(); - $scheduledBackup = ScheduledDatabaseBackup::find(0); - if (! $scheduledBackup) { - ScheduledDatabaseBackup::create([ - 'id' => 0, - 'enabled' => true, - 'save_s3' => false, - 'frequency' => '0 0 * * *', - 'database_id' => $database->id, - 'database_type' => 'App\Models\StandalonePostgresql', - 'team_id' => 0, - ]); + if (version_compare('4.0.0-beta.179', config('version'), '<=')) { + try { + $database = StandalonePostgresql::withTrashed()->find(0); + if ($database && $database->trashed()) { + echo "Restoring coolify db backup\n"; + $database->restore(); + $scheduledBackup = ScheduledDatabaseBackup::find(0); + if (! $scheduledBackup) { + ScheduledDatabaseBackup::create([ + 'id' => 0, + 'enabled' => true, + 'save_s3' => false, + 'frequency' => '0 0 * * *', + 'database_id' => $database->id, + 'database_type' => \App\Models\StandalonePostgresql::class, + 'team_id' => 0, + ]); + } } - } - } catch (\Throwable $e) { - echo "Error in restoring coolify db backup: {$e->getMessage()}\n"; - } - } - - private function cleanup_stucked_helper_containers() - { - $servers = Server::all(); - foreach ($servers as $server) { - if ($server->isFunctional()) { - CleanupHelperContainersJob::dispatch($server); + } catch (\Throwable $e) { + echo "Error in restoring coolify db backup: {$e->getMessage()}\n"; } } } - private function alive() + private function send_alive_signal() { $id = config('app.id'); $version = config('version'); - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $do_not_track = data_get($settings, 'do_not_track'); if ($do_not_track == true) { echo "Skipping alive as do_not_track is enabled\n"; @@ -114,34 +218,16 @@ class Init extends Command echo "Error in alive: {$e->getMessage()}\n"; } } - // private function cleanup_ssh() - // { - // TODO: it will cleanup id.root@host.docker.internal - // try { - // $files = Storage::allFiles('ssh/keys'); - // foreach ($files as $file) { - // Storage::delete($file); - // } - // $files = Storage::allFiles('ssh/mux'); - // foreach ($files as $file) { - // Storage::delete($file); - // } - // } catch (\Throwable $e) { - // echo "Error in cleaning ssh: {$e->getMessage()}\n"; - // } - // } private function cleanup_in_progress_application_deployments() { // Cleanup any failed deployments - try { if (isCloud()) { return; } $queued_inprogress_deployments = ApplicationDeploymentQueue::whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value])->get(); foreach ($queued_inprogress_deployments as $deployment) { - ray($deployment->id, $deployment->status); echo "Cleaning up deployment: {$deployment->id}\n"; $deployment->status = ApplicationDeploymentStatus::FAILED->value; $deployment->save(); @@ -150,4 +236,17 @@ class Init extends Command echo "Error: {$e->getMessage()}\n"; } } + + private function replace_slash_in_environment_name() + { + if (version_compare('4.0.0-beta.298', config('version'), '<=')) { + $environments = Environment::all(); + foreach ($environments as $environment) { + if (str_contains($environment->name, '/')) { + $environment->name = str_replace('/', '-', $environment->name); + $environment->save(); + } + } + } + } } diff --git a/app/Console/Commands/NotifyDemo.php b/app/Console/Commands/NotifyDemo.php index 81333b868..f0131b7b2 100644 --- a/app/Console/Commands/NotifyDemo.php +++ b/app/Console/Commands/NotifyDemo.php @@ -36,8 +36,6 @@ class NotifyDemo extends Command return; } - - ray($channel); } private function showHelp() diff --git a/app/Console/Commands/OpenApi.php b/app/Console/Commands/OpenApi.php new file mode 100644 index 000000000..e248aa2c0 --- /dev/null +++ b/app/Console/Commands/OpenApi.php @@ -0,0 +1,25 @@ +errorOutput(); + $error = preg_replace('/^.*an object literal,.*$/m', '', $error); + $error = preg_replace('/^\h*\v+/m', '', $error); + echo $error; + echo $process->output(); + } +} diff --git a/app/Console/Commands/ServicesDelete.php b/app/Console/Commands/ServicesDelete.php index b5a74166a..1e5d5808c 100644 --- a/app/Console/Commands/ServicesDelete.php +++ b/app/Console/Commands/ServicesDelete.php @@ -96,7 +96,7 @@ class ServicesDelete extends Command if (! $confirmed) { break; } - DeleteResourceJob::dispatch($toDelete); + DeleteResourceJob::dispatch($toDelete)->onQueue('high'); } } } @@ -122,7 +122,7 @@ class ServicesDelete extends Command if (! $confirmed) { return; } - DeleteResourceJob::dispatch($toDelete); + DeleteResourceJob::dispatch($toDelete)->onQueue('high'); } } } @@ -148,7 +148,7 @@ class ServicesDelete extends Command if (! $confirmed) { return; } - DeleteResourceJob::dispatch($toDelete); + DeleteResourceJob::dispatch($toDelete)->onQueue('high'); } } } diff --git a/app/Console/Commands/ServicesGenerate.php b/app/Console/Commands/ServicesGenerate.php index de64afefa..1559e5f6d 100644 --- a/app/Console/Commands/ServicesGenerate.php +++ b/app/Console/Commands/ServicesGenerate.php @@ -3,128 +3,82 @@ namespace App\Console\Commands; use Illuminate\Console\Command; +use Illuminate\Support\Arr; use Symfony\Component\Yaml\Yaml; class ServicesGenerate extends Command { /** - * The name and signature of the console command. - * - * @var string + * {@inheritdoc} */ protected $signature = 'services:generate'; /** - * The console command description. - * - * @var string + * {@inheritdoc} */ protected $description = 'Generate service-templates.yaml based on /templates/compose directory'; - /** - * Execute the console command. - */ - public function handle() + public function handle(): int { - $files = array_diff(scandir(base_path('templates/compose')), ['.', '..']); - $files = array_filter($files, function ($file) { - return strpos($file, '.yaml') !== false; - }); - $serviceTemplatesJson = []; - foreach ($files as $file) { - $parsed = $this->process_file($file); - if ($parsed) { - $name = data_get($parsed, 'name'); - $parsed = data_forget($parsed, 'name'); - $serviceTemplatesJson[$name] = $parsed; - } - } - $serviceTemplatesJson = json_encode($serviceTemplatesJson); - file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson); + $serviceTemplatesJson = collect(glob(base_path('templates/compose/*.yaml'))) + ->mapWithKeys(function ($file): array { + $file = basename($file); + $parsed = $this->processFile($file); + + return $parsed === false ? [] : [ + Arr::pull($parsed, 'name') => $parsed, + ]; + })->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + + file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson.PHP_EOL); + + return self::SUCCESS; } - private function process_file($file) + private function processFile(string $file): false|array { - $serviceName = str($file)->before('.yaml')->value(); $content = file_get_contents(base_path("templates/compose/$file")); - // $this->info($content); - $ignore = collect(preg_grep('/^# ignore:/', explode("\n", $content)))->values(); - if ($ignore->count() > 0) { - $ignore = (bool) str($ignore[0])->after('# ignore:')->trim()->value(); - } else { - $ignore = false; - } - if ($ignore) { + + $data = collect(explode(PHP_EOL, $content))->mapWithKeys(function ($line): array { + preg_match('/^#(?.*):(?.*)$/U', $line, $m); + + return $m ? [trim($m['key']) => trim($m['value'])] : []; + }); + + if (str($data->get('ignore'))->toBoolean()) { $this->info("Ignoring $file"); - return; + return false; } + $this->info("Processing $file"); - $documentation = collect(preg_grep('/^# documentation:/', explode("\n", $content)))->values(); - if ($documentation->count() > 0) { - $documentation = str($documentation[0])->after('# documentation:')->trim()->value(); - $documentation = str($documentation)->append('?utm_source=coolify.io'); - } else { - $documentation = 'https://coolify.io/docs'; - } - $slogan = collect(preg_grep('/^# slogan:/', explode("\n", $content)))->values(); - if ($slogan->count() > 0) { - $slogan = str($slogan[0])->after('# slogan:')->trim()->value(); - } else { - $slogan = str($file)->headline()->value(); - } - $logo = collect(preg_grep('/^# logo:/', explode("\n", $content)))->values(); - if ($logo->count() > 0) { - $logo = str($logo[0])->after('# logo:')->trim()->value(); - } else { - $logo = 'svgs/unknown.svg'; - } - $minversion = collect(preg_grep('/^# minversion:/', explode("\n", $content)))->values(); - if ($minversion->count() > 0) { - $minversion = str($minversion[0])->after('# minversion:')->trim()->value(); - } else { - $minversion = '0.0.0'; - } - $env_file = collect(preg_grep('/^# env_file:/', explode("\n", $content)))->values(); - if ($env_file->count() > 0) { - $env_file = str($env_file[0])->after('# env_file:')->trim()->value(); - } else { - $env_file = null; - } + $documentation = $data->get('documentation'); + $documentation = $documentation ? $documentation.'?utm_source=coolify.io' : 'https://coolify.io/docs'; - $tags = collect(preg_grep('/^# tags:/', explode("\n", $content)))->values(); - if ($tags->count() > 0) { - $tags = str($tags[0])->after('# tags:')->trim()->explode(',')->map(function ($tag) { - return str($tag)->trim()->lower()->value(); - })->values(); - } else { - $tags = null; - } - $port = collect(preg_grep('/^# port:/', explode("\n", $content)))->values(); - if ($port->count() > 0) { - $port = str($port[0])->after('# port:')->trim()->value(); - } else { - $port = null; - } $json = Yaml::parse($content); - $yaml = base64_encode(Yaml::dump($json, 10, 2)); + $compose = base64_encode(Yaml::dump($json, 10, 2)); + + $tags = str($data->get('tags'))->lower()->explode(',')->map(fn ($tag) => trim($tag))->filter(); + $tags = $tags->isEmpty() ? null : $tags->all(); + $payload = [ - 'name' => $serviceName, + 'name' => pathinfo($file, PATHINFO_FILENAME), 'documentation' => $documentation, - 'slogan' => $slogan, - 'compose' => $yaml, + 'slogan' => $data->get('slogan', str($file)->headline()), + 'compose' => $compose, 'tags' => $tags, - 'logo' => $logo, - 'minversion' => $minversion, + 'logo' => $data->get('logo', 'svgs/coolify.png'), + 'minversion' => $data->get('minversion', '0.0.0'), ]; - if ($port) { + + if ($port = $data->get('port')) { $payload['port'] = $port; } - if ($env_file) { - $env_file_content = file_get_contents(base_path("templates/compose/$env_file")); - $env_file_base64 = base64_encode($env_file_content); - $payload['envs'] = $env_file_base64; + + if ($envFile = $data->get('env_file')) { + $envFileContent = file_get_contents(base_path("templates/compose/$envFile")); + $payload['envs'] = base64_encode($envFileContent); } return $payload; diff --git a/app/Console/Commands/SyncBunny.php b/app/Console/Commands/SyncBunny.php index 7135cfc9c..228467f88 100644 --- a/app/Console/Commands/SyncBunny.php +++ b/app/Console/Commands/SyncBunny.php @@ -16,7 +16,7 @@ class SyncBunny extends Command * * @var string */ - protected $signature = 'sync:bunny {--templates} {--release}'; + protected $signature = 'sync:bunny {--templates} {--release} {--nightly}'; /** * The console command description. @@ -33,6 +33,7 @@ class SyncBunny extends Command $that = $this; $only_template = $this->option('templates'); $only_version = $this->option('release'); + $nightly = $this->option('nightly'); $bunny_cdn = 'https://cdn.coollabs.io'; $bunny_cdn_path = 'coolify'; $bunny_cdn_storage_name = 'coolcdn'; @@ -45,9 +46,15 @@ class SyncBunny extends Command $upgrade_script = 'upgrade.sh'; $production_env = '.env.production'; $service_template = 'service-templates.json'; - $versions = 'versions.json'; + $compose_file_location = "$parent_dir/$compose_file"; + $compose_file_prod_location = "$parent_dir/$compose_file_prod"; + $install_script_location = "$parent_dir/scripts/install.sh"; + $upgrade_script_location = "$parent_dir/scripts/upgrade.sh"; + $production_env_location = "$parent_dir/.env.production"; + $versions_location = "$parent_dir/$versions"; + PendingRequest::macro('storage', function ($fileName) use ($that) { $headers = [ 'AccessKey' => env('BUNNY_STORAGE_API_KEY'), @@ -73,8 +80,26 @@ class SyncBunny extends Command ]); }); try { + if ($nightly) { + $bunny_cdn_path = 'coolify-nightly'; + + $compose_file_location = "$parent_dir/other/nightly/$compose_file"; + $compose_file_prod_location = "$parent_dir/other/nightly/$compose_file_prod"; + $production_env_location = "$parent_dir/other/nightly/$production_env"; + $upgrade_script_location = "$parent_dir/other/nightly/$upgrade_script"; + $install_script_location = "$parent_dir/other/nightly/$install_script"; + $versions_location = "$parent_dir/other/nightly/$versions"; + } if (! $only_template && ! $only_version) { - $this->info('About to sync files (docker-compose.prod.yaml, upgrade.sh, install.sh, etc) to BunnyCDN.'); + if ($nightly) { + $this->info('About to sync files NIGHTLY (docker-compose.prod.yaml, upgrade.sh, install.sh, etc) to BunnyCDN.'); + } else { + $this->info('About to sync files PRODUCTION (docker-compose.yml, docker-compose.prod.yml, upgrade.sh, install.sh, etc) to BunnyCDN.'); + } + $confirmed = confirm('Are you sure you want to sync?'); + if (! $confirmed) { + return; + } } if ($only_template) { $this->info('About to sync service-templates.json to BunnyCDN.'); @@ -90,8 +115,12 @@ class SyncBunny extends Command return; } elseif ($only_version) { - $this->info('About to sync versions.json to BunnyCDN.'); - $file = file_get_contents("$parent_dir/$versions"); + if ($nightly) { + $this->info('About to sync NIGHLTY versions.json to BunnyCDN.'); + } else { + $this->info('About to sync PRODUCTION versions.json to BunnyCDN.'); + } + $file = file_get_contents($versions_location); $json = json_decode($file, true); $actual_version = data_get($json, 'coolify.v4.version'); @@ -100,7 +129,7 @@ class SyncBunny extends Command return; } Http::pool(fn (Pool $pool) => [ - $pool->storage(fileName: "$parent_dir/$versions")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"), + $pool->storage(fileName: $versions_location)->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"), $pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"), ]); $this->info('versions.json uploaded & purged...'); @@ -109,11 +138,11 @@ class SyncBunny extends Command } Http::pool(fn (Pool $pool) => [ - $pool->storage(fileName: "$parent_dir/$compose_file")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"), - $pool->storage(fileName: "$parent_dir/$compose_file_prod")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"), - $pool->storage(fileName: "$parent_dir/$production_env")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$production_env"), - $pool->storage(fileName: "$parent_dir/scripts/$upgrade_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$upgrade_script"), - $pool->storage(fileName: "$parent_dir/scripts/$install_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$install_script"), + $pool->storage(fileName: "$compose_file_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"), + $pool->storage(fileName: "$compose_file_prod_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"), + $pool->storage(fileName: "$production_env_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$production_env"), + $pool->storage(fileName: "$upgrade_script_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$upgrade_script"), + $pool->storage(fileName: "$install_script_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$install_script"), ]); Http::pool(fn (Pool $pool) => [ $pool->purge("$bunny_cdn/$bunny_cdn_path/$compose_file"), diff --git a/app/Console/Commands/WaitlistInvite.php b/app/Console/Commands/WaitlistInvite.php index 88ff21d46..2e330068c 100644 --- a/app/Console/Commands/WaitlistInvite.php +++ b/app/Console/Commands/WaitlistInvite.php @@ -82,7 +82,7 @@ class WaitlistInvite extends Command if (! $already_registered) { $this->password = Str::password(); User::create([ - 'name' => Str::of($this->next_patient->email)->before('@'), + 'name' => str($this->next_patient->email)->before('@'), 'email' => $this->next_patient->email, 'password' => Hash::make($this->password), 'force_password_reset' => true, @@ -103,7 +103,7 @@ class WaitlistInvite extends Command { $token = Crypt::encryptString("{$this->next_patient->email}@@@$this->password"); $loginLink = route('auth.link', ['token' => $token]); - $mail = new MailMessage(); + $mail = new MailMessage; $mail->view('emails.waitlist-invitation', [ 'loginLink' => $loginLink, ]); diff --git a/app/Console/Commands/Weird.php b/app/Console/Commands/Weird.php new file mode 100644 index 000000000..e471a5f96 --- /dev/null +++ b/app/Console/Commands/Weird.php @@ -0,0 +1,58 @@ +error('This command can only be run in development mode'); + + return; + } + $run = $this->option('run'); + if ($run) { + $servers = Server::all(); + foreach ($servers as $server) { + ServerCheck::dispatch($server); + } + + return; + } + $number = $this->option('number'); + for ($i = 0; $i < $number; $i++) { + $uuid = Str::uuid(); + $server = Server::create([ + 'name' => 'localhost-'.$uuid, + 'description' => 'This is a test docker container in development mode', + 'ip' => 'coolify-testing-host', + 'team_id' => 0, + 'private_key_id' => 1, + 'proxy' => [ + 'type' => ProxyTypes::NONE->value, + 'status' => ProxyStatus::EXITED->value, + ], + ]); + $server->settings->update([ + 'is_usable' => true, + 'is_reachable' => true, + ]); + } + } catch (\Exception $e) { + $this->error($e->getMessage()); + } + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index e2698d90e..3fb4de60b 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -2,135 +2,195 @@ namespace App\Console; -use App\Jobs\CheckLogDrainContainerJob; +use App\Jobs\CheckAndStartSentinelJob; +use App\Jobs\CheckForUpdatesJob; +use App\Jobs\CheckHelperImageJob; use App\Jobs\CleanupInstanceStuffsJob; -use App\Jobs\ContainerStatusJob; +use App\Jobs\CleanupStaleMultiplexedConnections; use App\Jobs\DatabaseBackupJob; -use App\Jobs\PullCoolifyImageJob; -use App\Jobs\PullHelperImageJob; -use App\Jobs\PullSentinelImageJob; +use App\Jobs\DockerCleanupJob; use App\Jobs\PullTemplatesFromCDN; use App\Jobs\ScheduledTaskJob; -use App\Jobs\ServerStatusJob; +use App\Jobs\ServerCheckJob; +use App\Jobs\ServerCleanupMux; +use App\Jobs\ServerStorageCheckJob; +use App\Jobs\UpdateCoolifyJob; +use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledTask; use App\Models\Server; use App\Models\Team; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; +use Illuminate\Support\Carbon; class Kernel extends ConsoleKernel { - private $all_servers; + private $allServers; + + private InstanceSettings $settings; + + private string $updateCheckFrequency; + + private string $instanceTimezone; protected function schedule(Schedule $schedule): void { - $this->all_servers = Server::all(); + $this->allServers = Server::where('ip', '!=', '1.2.3.4'); + + $this->settings = instanceSettings(); + $this->updateCheckFrequency = $this->settings->update_check_frequency ?: '0 * * * *'; + $this->instanceTimezone = $this->settings->instance_timezone ?: config('app.timezone'); + + $schedule->job(new CleanupStaleMultiplexedConnections)->hourly(); + if (isDev()) { // Instance Jobs $schedule->command('horizon:snapshot')->everyMinute(); $schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer(); - $schedule->job(new PullTemplatesFromCDN)->everyTwoHours()->onOneServer(); + $schedule->job(new CheckHelperImageJob)->everyTenMinutes()->onOneServer(); + // Server Jobs - $this->check_scheduled_backups($schedule); - $this->check_resources($schedule); - $this->check_scheduled_backups($schedule); - $this->check_scheduled_tasks($schedule); + $this->checkResources($schedule); + + $this->checkScheduledBackups($schedule); + $this->checkScheduledTasks($schedule); + $schedule->command('uploads:clear')->everyTwoMinutes(); + } else { // Instance Jobs $schedule->command('horizon:snapshot')->everyFiveMinutes(); - $schedule->command('cleanup:unreachable-servers')->daily(); - $schedule->job(new PullCoolifyImageJob)->everyTenMinutes()->onOneServer(); - $schedule->job(new PullTemplatesFromCDN)->everyThirtyMinutes()->onOneServer(); + $schedule->command('cleanup:unreachable-servers')->daily()->onOneServer(); + $schedule->job(new PullTemplatesFromCDN)->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer(); $schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer(); - // $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer(); + $this->scheduleUpdates($schedule); // Server Jobs - $this->check_scheduled_backups($schedule); - $this->check_resources($schedule); - $this->pull_images($schedule); - $this->check_scheduled_tasks($schedule); + $this->checkResources($schedule); + + $this->pullImages($schedule); + + $this->checkScheduledBackups($schedule); + $this->checkScheduledTasks($schedule); $schedule->command('cleanup:database --yes')->daily(); $schedule->command('uploads:clear')->everyTwoMinutes(); } } - private function pull_images($schedule) + private function pullImages($schedule): void { - $servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4'); + $servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get(); foreach ($servers as $server) { - if (config('coolify.is_sentinel_enabled')) { - $schedule->job(new PullSentinelImageJob($server))->everyFiveMinutes()->onOneServer(); + if ($server->isSentinelEnabled()) { + $schedule->job(function () use ($server) { + CheckAndStartSentinelJob::dispatch($server); + })->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer(); } - $schedule->job(new PullHelperImageJob($server))->everyFiveMinutes()->onOneServer(); + } + $schedule->job(new CheckHelperImageJob) + ->cron($this->updateCheckFrequency) + ->timezone($this->instanceTimezone) + ->onOneServer(); + } + + private function scheduleUpdates($schedule): void + { + $schedule->job(new CheckForUpdatesJob) + ->cron($this->updateCheckFrequency) + ->timezone($this->instanceTimezone) + ->onOneServer(); + + if ($this->settings->is_auto_update_enabled) { + $autoUpdateFrequency = $this->settings->auto_update_frequency; + $schedule->job(new UpdateCoolifyJob) + ->cron($autoUpdateFrequency) + ->timezone($this->instanceTimezone) + ->onOneServer(); } } - private function check_resources($schedule) + private function checkResources($schedule): void { if (isCloud()) { - $servers = $this->all_servers->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4'); + $servers = $this->allServers->whereHas('team.subscription')->get(); $own = Team::find(0)->servers; $servers = $servers->merge($own); - $containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false); } else { - $servers = $this->all_servers->where('ip', '!=', '1.2.3.4'); - $containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false); - } - foreach ($containerServers as $server) { - $schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer(); - if ($server->isLogDrainEnabled()) { - $schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer(); - } + $servers = $this->allServers->get(); } + foreach ($servers as $server) { - $schedule->job(new ServerStatusJob($server))->everyMinute()->onOneServer(); + $serverTimezone = $server->settings->server_timezone; + + // Sentinel check + $lastSentinelUpdate = $server->sentinel_updated_at; + if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) { + // Check container status every minute if Sentinel does not activated + $schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer(); + // $schedule->job(new \App\Jobs\ServerCheckNewJob($server))->everyMinute()->onOneServer(); + + // Check storage usage every 10 minutes if Sentinel does not activated + $schedule->job(new ServerStorageCheckJob($server))->everyTenMinutes()->onOneServer(); + } + if ($server->settings->force_docker_cleanup) { + $schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer(); + } else { + $schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer(); + } + + // Cleanup multiplexed connections every hour + $schedule->job(new ServerCleanupMux($server))->hourly()->onOneServer(); + + // Temporary solution until we have better memory management for Sentinel + if ($server->isSentinelEnabled()) { + $schedule->job(function () use ($server) { + $server->restartContainer('coolify-sentinel'); + })->daily()->onOneServer(); + } } } - private function check_scheduled_backups($schedule) + private function checkScheduledBackups($schedule): void { - $scheduled_backups = ScheduledDatabaseBackup::all(); + $scheduled_backups = ScheduledDatabaseBackup::where('enabled', true)->get(); if ($scheduled_backups->isEmpty()) { return; } foreach ($scheduled_backups as $scheduled_backup) { - if (! $scheduled_backup->enabled) { - continue; - } if (is_null(data_get($scheduled_backup, 'database'))) { - ray('database not found'); $scheduled_backup->delete(); continue; } + $server = $scheduled_backup->server(); + + if (is_null($server)) { + continue; + } + if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) { $scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency]; } $schedule->job(new DatabaseBackupJob( backup: $scheduled_backup - ))->cron($scheduled_backup->frequency)->onOneServer(); + ))->cron($scheduled_backup->frequency)->timezone($this->instanceTimezone)->onOneServer(); } } - private function check_scheduled_tasks($schedule) + private function checkScheduledTasks($schedule): void { - $scheduled_tasks = ScheduledTask::all(); + $scheduled_tasks = ScheduledTask::where('enabled', true)->get(); if ($scheduled_tasks->isEmpty()) { return; } foreach ($scheduled_tasks as $scheduled_task) { - if ($scheduled_task->enabled === false) { - continue; - } $service = $scheduled_task->service; $application = $scheduled_task->application; if (! $application && ! $service) { - ray('application/service attached to scheduled task does not exist'); $scheduled_task->delete(); continue; @@ -145,12 +205,18 @@ class Kernel extends ConsoleKernel continue; } } + + $server = $scheduled_task->server(); + if (! $server) { + continue; + } + if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) { $scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency]; } $schedule->job(new ScheduledTaskJob( task: $scheduled_task - ))->cron($scheduled_task->frequency)->onOneServer(); + ))->cron($scheduled_task->frequency)->timezone($this->instanceTimezone)->onOneServer(); } } diff --git a/app/Data/ServerMetadata.php b/app/Data/ServerMetadata.php index b96efa622..d95944b15 100644 --- a/app/Data/ServerMetadata.php +++ b/app/Data/ServerMetadata.php @@ -11,6 +11,5 @@ class ServerMetadata extends Data public function __construct( public ?ProxyTypes $type, public ?ProxyStatus $status - ) { - } + ) {} } diff --git a/app/Enums/ActivityTypes.php b/app/Enums/ActivityTypes.php index e2536a7f0..2d23cd98b 100644 --- a/app/Enums/ActivityTypes.php +++ b/app/Enums/ActivityTypes.php @@ -5,4 +5,5 @@ namespace App\Enums; enum ActivityTypes: string { case INLINE = 'inline'; + case COMMAND = 'command'; } diff --git a/app/Enums/BuildPackTypes.php b/app/Enums/BuildPackTypes.php new file mode 100644 index 000000000..cb51db6d6 --- /dev/null +++ b/app/Enums/BuildPackTypes.php @@ -0,0 +1,11 @@ + 1, + self::ADMIN => 2, + self::OWNER => 3, + }; + } + + public function lt(Role|string $role): bool + { + if (is_string($role)) { + $role = Role::from($role); + } + + return $this->rank() < $role->rank(); + } + + public function gt(Role|string $role): bool + { + if (is_string($role)) { + $role = Role::from($role); + } + + return $this->rank() > $role->rank(); + } +} diff --git a/app/Enums/StaticImageTypes.php b/app/Enums/StaticImageTypes.php new file mode 100644 index 000000000..5f5304bf8 --- /dev/null +++ b/app/Enums/StaticImageTypes.php @@ -0,0 +1,8 @@ +user()->currentTeam()->id ?? null; + } + if (is_null($teamId)) { + throw new \Exception('Team id is null'); + } + $this->teamId = $teamId; + } + + public function broadcastOn(): array + { + return [ + new PrivateChannel("team.{$this->teamId}"), + ]; + } +} diff --git a/app/Events/DatabaseProxyStopped.php b/app/Events/DatabaseProxyStopped.php new file mode 100644 index 000000000..b457dc6a0 --- /dev/null +++ b/app/Events/DatabaseProxyStopped.php @@ -0,0 +1,35 @@ +currentTeam()->id ?? null; + } + if (is_null($teamId)) { + throw new \Exception('Team id is null'); + } + $this->teamId = $teamId; + } + + public function broadcastOn(): array + { + return [ + new PrivateChannel("team.{$this->teamId}"), + ]; + } +} diff --git a/app/Events/DatabaseStatusChanged.php b/app/Events/DatabaseStatusChanged.php index 190983c80..913b21bc2 100644 --- a/app/Events/DatabaseStatusChanged.php +++ b/app/Events/DatabaseStatusChanged.php @@ -7,28 +7,34 @@ use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Auth; class DatabaseStatusChanged implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; - public $userId; + public $userId = null; public function __construct($userId = null) { if (is_null($userId)) { - $userId = auth()->user()->id ?? null; + $userId = Auth::id() ?? null; } if (is_null($userId)) { - throw new \Exception('User id is null'); + return false; } + $this->userId = $userId; } - public function broadcastOn(): array + public function broadcastOn(): ?array { - return [ - new PrivateChannel("user.{$this->userId}"), - ]; + if (! is_null($this->userId)) { + return [ + new PrivateChannel("user.{$this->userId}"), + ]; + } + + return null; } } diff --git a/app/Events/FileStorageChanged.php b/app/Events/FileStorageChanged.php new file mode 100644 index 000000000..57004cf4c --- /dev/null +++ b/app/Events/FileStorageChanged.php @@ -0,0 +1,31 @@ +teamId = $teamId; + } + + public function broadcastOn(): array + { + return [ + new PrivateChannel("team.{$this->teamId}"), + ]; + } +} diff --git a/app/Events/ProxyStarted.php b/app/Events/ProxyStarted.php index ed62eccb1..64d562e0a 100644 --- a/app/Events/ProxyStarted.php +++ b/app/Events/ProxyStarted.php @@ -10,8 +10,5 @@ class ProxyStarted { use Dispatchable, InteractsWithSockets, SerializesModels; - public function __construct(public $data) - { - - } + public function __construct(public $data) {} } diff --git a/app/Events/ScheduledTaskDone.php b/app/Events/ScheduledTaskDone.php new file mode 100644 index 000000000..c8b5547f6 --- /dev/null +++ b/app/Events/ScheduledTaskDone.php @@ -0,0 +1,34 @@ +user()->currentTeam()->id ?? null; + } + if (is_null($teamId)) { + throw new \Exception('Team id is null'); + } + $this->teamId = $teamId; + } + + public function broadcastOn(): array + { + return [ + new PrivateChannel("team.{$this->teamId}"), + ]; + } +} diff --git a/app/Events/ServiceStatusChanged.php b/app/Events/ServiceStatusChanged.php index e3e24a248..3950022e1 100644 --- a/app/Events/ServiceStatusChanged.php +++ b/app/Events/ServiceStatusChanged.php @@ -7,28 +7,33 @@ use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Auth; class ServiceStatusChanged implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; - public $userId; + public ?string $userId = null; public function __construct($userId = null) { if (is_null($userId)) { - $userId = auth()->user()->id ?? null; + $userId = Auth::id() ?? null; } if (is_null($userId)) { - throw new \Exception('User id is null'); + return false; } $this->userId = $userId; } - public function broadcastOn(): array + public function broadcastOn(): ?array { - return [ - new PrivateChannel("user.{$this->userId}"), - ]; + if (! is_null($this->userId)) { + return [ + new PrivateChannel("user.{$this->userId}"), + ]; + } + + return null; } } diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 254a8df7a..8c89bb07f 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -50,7 +50,7 @@ class Handler extends ExceptionHandler return response()->json(['message' => $exception->getMessage()], 401); } - return redirect()->guest($exception->redirectTo() ?? route('login')); + return redirect()->guest($exception->redirectTo($request) ?? route('login')); } /** @@ -65,7 +65,7 @@ class Handler extends ExceptionHandler if ($e instanceof RuntimeException) { return; } - $this->settings = InstanceSettings::get(); + $this->settings = instanceSettings(); if ($this->settings->do_not_track) { return; } @@ -84,7 +84,6 @@ class Handler extends ExceptionHandler if (str($e->getMessage())->contains('No space left on device')) { return; } - ray('reporting to sentry'); Integration::captureUnhandledException($e); }); } diff --git a/app/Exceptions/ProcessException.php b/app/Exceptions/ProcessException.php index 728a0d81b..47eaa6fd8 100644 --- a/app/Exceptions/ProcessException.php +++ b/app/Exceptions/ProcessException.php @@ -4,6 +4,4 @@ namespace App\Exceptions; use Exception; -class ProcessException extends Exception -{ -} +class ProcessException extends Exception {} diff --git a/app/Helpers/SshMultiplexingHelper.php b/app/Helpers/SshMultiplexingHelper.php new file mode 100644 index 000000000..1a2146799 --- /dev/null +++ b/app/Helpers/SshMultiplexingHelper.php @@ -0,0 +1,186 @@ +private_key_id); + $sshKeyLocation = $privateKey->getKeyLocation(); + $muxFilename = '/var/www/html/storage/app/ssh/mux/mux_'.$server->uuid; + + return [ + 'sshKeyLocation' => $sshKeyLocation, + 'muxFilename' => $muxFilename, + ]; + } + + public static function ensureMultiplexedConnection(Server $server) + { + if (! self::isMultiplexingEnabled()) { + return; + } + + $sshConfig = self::serverSshConfiguration($server); + $muxSocket = $sshConfig['muxFilename']; + $sshKeyLocation = $sshConfig['sshKeyLocation']; + + self::validateSshKey($sshKeyLocation); + + $checkCommand = "ssh -O check -o ControlPath=$muxSocket "; + if (data_get($server, 'settings.is_cloudflare_tunnel')) { + $checkCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" '; + } + $checkCommand .= "{$server->user}@{$server->ip}"; + $process = Process::run($checkCommand); + + if ($process->exitCode() !== 0) { + self::establishNewMultiplexedConnection($server); + } + } + + public static function establishNewMultiplexedConnection(Server $server) + { + $sshConfig = self::serverSshConfiguration($server); + $sshKeyLocation = $sshConfig['sshKeyLocation']; + $muxSocket = $sshConfig['muxFilename']; + + $connectionTimeout = config('constants.ssh.connection_timeout'); + $serverInterval = config('constants.ssh.server_interval'); + $muxPersistTime = config('constants.ssh.mux_persist_time'); + + $establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; + + if (data_get($server, 'settings.is_cloudflare_tunnel')) { + $establishCommand .= ' -o ProxyCommand="cloudflared access ssh --hostname %h" '; + } + + $establishCommand .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval); + $establishCommand .= "{$server->user}@{$server->ip}"; + + $establishProcess = Process::run($establishCommand); + + if ($establishProcess->exitCode() !== 0) { + throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput()); + } + } + + public static function removeMuxFile(Server $server) + { + $sshConfig = self::serverSshConfiguration($server); + $muxSocket = $sshConfig['muxFilename']; + + $closeCommand = "ssh -O exit -o ControlPath=$muxSocket "; + if (data_get($server, 'settings.is_cloudflare_tunnel')) { + $closeCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" '; + } + $closeCommand .= "{$server->user}@{$server->ip}"; + Process::run($closeCommand); + } + + public static function generateScpCommand(Server $server, string $source, string $dest) + { + $sshConfig = self::serverSshConfiguration($server); + $sshKeyLocation = $sshConfig['sshKeyLocation']; + $muxSocket = $sshConfig['muxFilename']; + + $timeout = config('constants.ssh.command_timeout'); + $muxPersistTime = config('constants.ssh.mux_persist_time'); + + $scp_command = "timeout $timeout scp "; + if ($server->isIpv6()) { + $scp_command .= '-6 '; + } + if (self::isMultiplexingEnabled()) { + $scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; + self::ensureMultiplexedConnection($server); + } + + if (data_get($server, 'settings.is_cloudflare_tunnel')) { + $scp_command .= '-o ProxyCommand="cloudflared access ssh --hostname %h" '; + } + + $scp_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'), isScp: true); + $scp_command .= "{$source} {$server->user}@{$server->ip}:{$dest}"; + + return $scp_command; + } + + public static function generateSshCommand(Server $server, string $command) + { + if ($server->settings->force_disabled) { + throw new \RuntimeException('Server is disabled.'); + } + + $sshConfig = self::serverSshConfiguration($server); + $sshKeyLocation = $sshConfig['sshKeyLocation']; + $muxSocket = $sshConfig['muxFilename']; + + $timeout = config('constants.ssh.command_timeout'); + $muxPersistTime = config('constants.ssh.mux_persist_time'); + + $ssh_command = "timeout $timeout ssh "; + + if (self::isMultiplexingEnabled()) { + $ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; + self::ensureMultiplexedConnection($server); + } + + if (data_get($server, 'settings.is_cloudflare_tunnel')) { + $ssh_command .= "-o ProxyCommand='cloudflared access ssh --hostname %h' "; + } + + $ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval')); + + $delimiter = Hash::make($command); + $delimiter = base64_encode($delimiter); + $command = str_replace($delimiter, '', $command); + + $ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL + .$command.PHP_EOL + .$delimiter; + + return $ssh_command; + } + + private static function isMultiplexingEnabled(): bool + { + return config('constants.ssh.mux_enabled') && ! config('coolify.is_windows_docker_desktop'); + } + + private static function validateSshKey(string $sshKeyLocation): void + { + $checkKeyCommand = "ls $sshKeyLocation 2>/dev/null"; + $keyCheckProcess = Process::run($checkKeyCommand); + + if ($keyCheckProcess->exitCode() !== 0) { + throw new \RuntimeException("SSH key file not accessible: $sshKeyLocation"); + } + } + + private static function getCommonSshOptions(Server $server, string $sshKeyLocation, int $connectionTimeout, int $serverInterval, bool $isScp = false): string + { + $options = "-i {$sshKeyLocation} " + .'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ' + .'-o PasswordAuthentication=no ' + ."-o ConnectTimeout=$connectionTimeout " + ."-o ServerAliveInterval=$serverInterval " + .'-o RequestTTY=no ' + .'-o LogLevel=ERROR '; + + // Bruh + if ($isScp) { + $options .= "-P {$server->port} "; + } else { + $options .= "-p {$server->port} "; + } + + return $options; + } +} diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php new file mode 100644 index 000000000..500db3922 --- /dev/null +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -0,0 +1,2813 @@ +user()->currentAccessToken(); + $application->makeHidden([ + 'id', + ]); + if ($token->can('view:sensitive')) { + return serializeApiResponse($application); + } + $application->makeHidden([ + 'custom_labels', + 'dockerfile', + 'docker_compose', + 'docker_compose_raw', + 'manual_webhook_secret_bitbucket', + 'manual_webhook_secret_gitea', + 'manual_webhook_secret_github', + 'manual_webhook_secret_gitlab', + 'private_key_id', + 'value', + 'real_value', + ]); + + return serializeApiResponse($application); + } + + #[OA\Get( + summary: 'List', + description: 'List all applications.', + path: '/applications', + operationId: 'list-applications', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + responses: [ + new OA\Response( + response: 200, + description: 'Get all applications.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/Application') + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function applications(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $projects = Project::where('team_id', $teamId)->get(); + $applications = collect(); + $applications->push($projects->pluck('applications')->flatten()); + $applications = $applications->flatten(); + $applications = $applications->map(function ($application) { + return $this->removeSensitiveData($application); + }); + + return response()->json($applications); + } + + #[OA\Post( + summary: 'Create (Public)', + description: 'Create new application based on a public git repository.', + path: '/applications/public', + operationId: 'create-public-application', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + requestBody: new OA\RequestBody( + description: 'Application object that needs to be created.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['project_uuid', 'server_uuid', 'environment_name', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'], + properties: [ + 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'], + 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'], + 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'], + 'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'], + 'git_branch' => ['type' => 'string', 'description' => 'The git branch.'], + 'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'], + 'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'], + 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'], + 'name' => ['type' => 'string', 'description' => 'The application name.'], + 'description' => ['type' => 'string', 'description' => 'The application description.'], + 'domains' => ['type' => 'string', 'description' => 'The application domains.'], + 'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'], + 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], + 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], + 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'], + 'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'], + 'install_command' => ['type' => 'string', 'description' => 'The install command.'], + 'build_command' => ['type' => 'string', 'description' => 'The build command.'], + 'start_command' => ['type' => 'string', 'description' => 'The start command.'], + 'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'], + 'base_directory' => ['type' => 'string', 'description' => 'The base directory for all commands.'], + 'publish_directory' => ['type' => 'string', 'description' => 'The publish directory.'], + 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'], + 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'], + 'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'], + 'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'], + 'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'], + 'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'], + 'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'], + 'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'], + 'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'], + 'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'], + 'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'], + 'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'], + 'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'], + 'custom_labels' => ['type' => 'string', 'description' => 'Custom labels.'], + 'custom_docker_run_options' => ['type' => 'string', 'description' => 'Custom docker run options.'], + 'post_deployment_command' => ['type' => 'string', 'description' => 'Post deployment command.'], + 'post_deployment_command_container' => ['type' => 'string', 'description' => 'Post deployment command container.'], + 'pre_deployment_command' => ['type' => 'string', 'description' => 'Pre deployment command.'], + 'pre_deployment_command_container' => ['type' => 'string', 'description' => 'Pre deployment command container.'], + 'manual_webhook_secret_github' => ['type' => 'string', 'description' => 'Manual webhook secret for Github.'], + 'manual_webhook_secret_gitlab' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitlab.'], + 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'description' => 'Manual webhook secret for Bitbucket.'], + 'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'], + 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], + // 'github_app_uuid' => ['type' => 'string', 'description' => 'The Github App UUID.'], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], + 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'], + 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'], + 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'], + 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], + 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], + 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], + 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], + ], + )), + ]), + responses: [ + new OA\Response( + response: 200, + description: 'Application created successfully.', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_public_application(Request $request) + { + return $this->create_application($request, 'public'); + } + + #[OA\Post( + summary: 'Create (Private - GH App)', + description: 'Create new application based on a private repository through a Github App.', + path: '/applications/private-github-app', + operationId: 'create-private-github-app-application', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + requestBody: new OA\RequestBody( + description: 'Application object that needs to be created.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['project_uuid', 'server_uuid', 'environment_name', 'github_app_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'], + properties: [ + 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'], + 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'], + 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'], + 'github_app_uuid' => ['type' => 'string', 'description' => 'The Github App UUID.'], + 'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'], + 'git_branch' => ['type' => 'string', 'description' => 'The git branch.'], + 'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'], + 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'], + 'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'], + 'name' => ['type' => 'string', 'description' => 'The application name.'], + 'description' => ['type' => 'string', 'description' => 'The application description.'], + 'domains' => ['type' => 'string', 'description' => 'The application domains.'], + 'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'], + 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], + 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], + 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'], + 'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'], + 'install_command' => ['type' => 'string', 'description' => 'The install command.'], + 'build_command' => ['type' => 'string', 'description' => 'The build command.'], + 'start_command' => ['type' => 'string', 'description' => 'The start command.'], + 'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'], + 'base_directory' => ['type' => 'string', 'description' => 'The base directory for all commands.'], + 'publish_directory' => ['type' => 'string', 'description' => 'The publish directory.'], + 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'], + 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'], + 'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'], + 'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'], + 'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'], + 'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'], + 'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'], + 'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'], + 'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'], + 'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'], + 'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'], + 'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'], + 'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'], + 'custom_labels' => ['type' => 'string', 'description' => 'Custom labels.'], + 'custom_docker_run_options' => ['type' => 'string', 'description' => 'Custom docker run options.'], + 'post_deployment_command' => ['type' => 'string', 'description' => 'Post deployment command.'], + 'post_deployment_command_container' => ['type' => 'string', 'description' => 'Post deployment command container.'], + 'pre_deployment_command' => ['type' => 'string', 'description' => 'Pre deployment command.'], + 'pre_deployment_command_container' => ['type' => 'string', 'description' => 'Pre deployment command container.'], + 'manual_webhook_secret_github' => ['type' => 'string', 'description' => 'Manual webhook secret for Github.'], + 'manual_webhook_secret_gitlab' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitlab.'], + 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'description' => 'Manual webhook secret for Bitbucket.'], + 'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'], + 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], + 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'], + 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'], + 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'], + 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], + 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], + 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], + 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], + ], + )), + ]), + responses: [ + new OA\Response( + response: 200, + description: 'Application created successfully.', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_private_gh_app_application(Request $request) + { + return $this->create_application($request, 'private-gh-app'); + } + + #[OA\Post( + summary: 'Create (Private - Deploy Key)', + description: 'Create new application based on a private repository through a Deploy Key.', + path: '/applications/private-deploy-key', + operationId: 'create-private-deploy-key-application', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + requestBody: new OA\RequestBody( + description: 'Application object that needs to be created.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['project_uuid', 'server_uuid', 'environment_name', 'private_key_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'], + properties: [ + 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'], + 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'], + 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'], + 'private_key_uuid' => ['type' => 'string', 'description' => 'The private key UUID.'], + 'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'], + 'git_branch' => ['type' => 'string', 'description' => 'The git branch.'], + 'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'], + 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'], + 'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'], + 'name' => ['type' => 'string', 'description' => 'The application name.'], + 'description' => ['type' => 'string', 'description' => 'The application description.'], + 'domains' => ['type' => 'string', 'description' => 'The application domains.'], + 'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'], + 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], + 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], + 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'], + 'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'], + 'install_command' => ['type' => 'string', 'description' => 'The install command.'], + 'build_command' => ['type' => 'string', 'description' => 'The build command.'], + 'start_command' => ['type' => 'string', 'description' => 'The start command.'], + 'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'], + 'base_directory' => ['type' => 'string', 'description' => 'The base directory for all commands.'], + 'publish_directory' => ['type' => 'string', 'description' => 'The publish directory.'], + 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'], + 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'], + 'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'], + 'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'], + 'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'], + 'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'], + 'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'], + 'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'], + 'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'], + 'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'], + 'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'], + 'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'], + 'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'], + 'custom_labels' => ['type' => 'string', 'description' => 'Custom labels.'], + 'custom_docker_run_options' => ['type' => 'string', 'description' => 'Custom docker run options.'], + 'post_deployment_command' => ['type' => 'string', 'description' => 'Post deployment command.'], + 'post_deployment_command_container' => ['type' => 'string', 'description' => 'Post deployment command container.'], + 'pre_deployment_command' => ['type' => 'string', 'description' => 'Pre deployment command.'], + 'pre_deployment_command_container' => ['type' => 'string', 'description' => 'Pre deployment command container.'], + 'manual_webhook_secret_github' => ['type' => 'string', 'description' => 'Manual webhook secret for Github.'], + 'manual_webhook_secret_gitlab' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitlab.'], + 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'description' => 'Manual webhook secret for Bitbucket.'], + 'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'], + 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], + 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'], + 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'], + 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'], + 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], + 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], + 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], + 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], + ], + )), + ]), + responses: [ + new OA\Response( + response: 200, + description: 'Application created successfully.', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_private_deploy_key_application(Request $request) + { + return $this->create_application($request, 'private-deploy-key'); + } + + #[OA\Post( + summary: 'Create (Dockerfile)', + description: 'Create new application based on a simple Dockerfile.', + path: '/applications/dockerfile', + operationId: 'create-dockerfile-application', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + requestBody: new OA\RequestBody( + description: 'Application object that needs to be created.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['project_uuid', 'server_uuid', 'environment_name', 'dockerfile'], + properties: [ + 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'], + 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'], + 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'], + 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], + 'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'], + 'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'], + 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'], + 'name' => ['type' => 'string', 'description' => 'The application name.'], + 'description' => ['type' => 'string', 'description' => 'The application description.'], + 'domains' => ['type' => 'string', 'description' => 'The application domains.'], + 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], + 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], + 'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'], + 'base_directory' => ['type' => 'string', 'description' => 'The base directory for all commands.'], + 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'], + 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'], + 'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'], + 'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'], + 'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'], + 'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'], + 'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'], + 'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'], + 'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'], + 'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'], + 'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'], + 'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'], + 'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'], + 'custom_labels' => ['type' => 'string', 'description' => 'Custom labels.'], + 'custom_docker_run_options' => ['type' => 'string', 'description' => 'Custom docker run options.'], + 'post_deployment_command' => ['type' => 'string', 'description' => 'Post deployment command.'], + 'post_deployment_command_container' => ['type' => 'string', 'description' => 'Post deployment command container.'], + 'pre_deployment_command' => ['type' => 'string', 'description' => 'Pre deployment command.'], + 'pre_deployment_command_container' => ['type' => 'string', 'description' => 'Pre deployment command container.'], + 'manual_webhook_secret_github' => ['type' => 'string', 'description' => 'Manual webhook secret for Github.'], + 'manual_webhook_secret_gitlab' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitlab.'], + 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'description' => 'Manual webhook secret for Bitbucket.'], + 'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'], + 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], + ], + )), + ]), + responses: [ + new OA\Response( + response: 200, + description: 'Application created successfully.', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_dockerfile_application(Request $request) + { + return $this->create_application($request, 'dockerfile'); + } + + #[OA\Post( + summary: 'Create (Docker Image)', + description: 'Create new application based on a prebuilt docker image', + path: '/applications/dockerimage', + operationId: 'create-dockerimage-application', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + requestBody: new OA\RequestBody( + description: 'Application object that needs to be created.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['project_uuid', 'server_uuid', 'environment_name', 'docker_registry_image_name', 'ports_exposes'], + properties: [ + 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'], + 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'], + 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'], + 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], + 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], + 'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'], + 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'], + 'name' => ['type' => 'string', 'description' => 'The application name.'], + 'description' => ['type' => 'string', 'description' => 'The application description.'], + 'domains' => ['type' => 'string', 'description' => 'The application domains.'], + 'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'], + 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'], + 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'], + 'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'], + 'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'], + 'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'], + 'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'], + 'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'], + 'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'], + 'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'], + 'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'], + 'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'], + 'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'], + 'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'], + 'custom_labels' => ['type' => 'string', 'description' => 'Custom labels.'], + 'custom_docker_run_options' => ['type' => 'string', 'description' => 'Custom docker run options.'], + 'post_deployment_command' => ['type' => 'string', 'description' => 'Post deployment command.'], + 'post_deployment_command_container' => ['type' => 'string', 'description' => 'Post deployment command container.'], + 'pre_deployment_command' => ['type' => 'string', 'description' => 'Pre deployment command.'], + 'pre_deployment_command_container' => ['type' => 'string', 'description' => 'Pre deployment command container.'], + 'manual_webhook_secret_github' => ['type' => 'string', 'description' => 'Manual webhook secret for Github.'], + 'manual_webhook_secret_gitlab' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitlab.'], + 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'description' => 'Manual webhook secret for Bitbucket.'], + 'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'], + 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], + ], + )), + ]), + responses: [ + new OA\Response( + response: 200, + description: 'Application created successfully.', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_dockerimage_application(Request $request) + { + return $this->create_application($request, 'dockerimage'); + } + + #[OA\Post( + summary: 'Create (Docker Compose)', + description: 'Create new application based on a docker-compose file.', + path: '/applications/dockercompose', + operationId: 'create-dockercompose-application', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + requestBody: new OA\RequestBody( + description: 'Application object that needs to be created.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['project_uuid', 'server_uuid', 'environment_name', 'docker_compose_raw'], + properties: [ + 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'], + 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'], + 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'], + 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'], + 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID if the server has more than one destinations.'], + 'name' => ['type' => 'string', 'description' => 'The application name.'], + 'description' => ['type' => 'string', 'description' => 'The application description.'], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], + ], + )), + ]), + responses: [ + new OA\Response( + response: 200, + description: 'Application created successfully.', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_dockercompose_application(Request $request) + { + return $this->create_application($request, 'dockercompose'); + } + + private function create_application(Request $request, $type) + { + $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration']; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255', + 'description' => 'string|nullable', + 'project_uuid' => 'string|required', + 'environment_name' => 'string|required', + 'server_uuid' => 'string|required', + 'destination_uuid' => 'string', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + + $serverUuid = $request->server_uuid; + $fqdn = $request->domains; + $instantDeploy = $request->instant_deploy; + $githubAppUuid = $request->github_app_uuid; + $useBuildServer = $request->use_build_server; + $isStatic = $request->is_static; + $customNginxConfiguration = $request->custom_nginx_configuration; + + if (! is_null($customNginxConfiguration)) { + if (! isBase64Encoded($customNginxConfiguration)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.', + ], + ], 422); + } + $customNginxConfiguration = base64_decode($customNginxConfiguration); + if (mb_detect_encoding($customNginxConfiguration, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.', + ], + ], 422); + } + } + + $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); + if (! $project) { + return response()->json(['message' => 'Project not found.'], 404); + } + $environment = $project->environments()->where('name', $request->environment_name)->first(); + if (! $environment) { + return response()->json(['message' => 'Environment not found.'], 404); + } + $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first(); + if (! $server) { + return response()->json(['message' => 'Server not found.'], 404); + } + $destinations = $server->destinations(); + if ($destinations->count() == 0) { + return response()->json(['message' => 'Server has no destinations.'], 400); + } + if ($destinations->count() > 1 && ! $request->has('destination_uuid')) { + return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400); + } + $destination = $destinations->first(); + if ($type === 'public') { + if (! $request->has('name')) { + $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); + } + if ($request->build_pack === 'dockercompose') { + $request->offsetSet('ports_exposes', '80'); + } + $validationRules = [ + 'git_repository' => 'string|required', + 'git_branch' => 'string|required', + 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)], + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', + 'docker_compose_location' => 'string', + 'docker_compose_raw' => 'string|nullable', + 'docker_compose_domains' => 'array|nullable', + ]; + $validationRules = array_merge($validationRules, sharedDataApplications()); + $validator = customApiValidator($request->all(), $validationRules); + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + + $application = new Application; + removeUnnecessaryFieldsFromRequest($request); + + $application->fill($request->all()); + $dockerComposeDomainsJson = collect(); + if ($request->has('docker_compose_domains')) { + $dockerComposeDomains = collect($request->docker_compose_domains); + if ($dockerComposeDomains->count() > 0) { + $dockerComposeDomains->each(function ($domain, $key) use ($dockerComposeDomainsJson) { + $dockerComposeDomainsJson->put(data_get($domain, 'name'), ['domain' => data_get($domain, 'domain')]); + }); + } + $request->offsetUnset('docker_compose_domains'); + } + if ($dockerComposeDomainsJson->count() > 0) { + $application->docker_compose_domains = $dockerComposeDomainsJson; + } + + $application->fqdn = $fqdn; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + $application->save(); + if (isset($isStatic)) { + $application->settings->is_static = $isStatic; + $application->settings->save(); + } + if (isset($useBuildServer)) { + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); + } + $application->refresh(); + if (! $application->settings->is_container_label_readonly_enabled) { + $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); + $application->save(); + } + $application->isConfigurationChanged(true); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2; + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } else { + if ($application->build_pack === 'dockercompose') { + LoadComposeFile::dispatch($application); + } + } + + return response()->json(serializeApiResponse([ + 'uuid' => data_get($application, 'uuid'), + 'domains' => data_get($application, 'domains'), + ])); + } elseif ($type === 'private-gh-app') { + if (! $request->has('name')) { + $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); + } + if ($request->build_pack === 'dockercompose') { + $request->offsetSet('ports_exposes', '80'); + } + $validationRules = [ + 'git_repository' => 'string|required', + 'git_branch' => 'string|required', + 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)], + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', + 'github_app_uuid' => 'string|required', + 'watch_paths' => 'string|nullable', + 'docker_compose_location' => 'string', + 'docker_compose_raw' => 'string|nullable', + ]; + $validationRules = array_merge($validationRules, sharedDataApplications()); + + $validator = customApiValidator($request->all(), $validationRules); + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $githubApp = GithubApp::whereTeamId($teamId)->where('uuid', $githubAppUuid)->first(); + if (! $githubApp) { + return response()->json(['message' => 'Github App not found.'], 404); + } + $gitRepository = $request->git_repository; + if (str($gitRepository)->startsWith('http') || str($gitRepository)->contains('github.com')) { + $gitRepository = str($gitRepository)->replace('https://', '')->replace('http://', '')->replace('github.com/', ''); + } + $application = new Application; + removeUnnecessaryFieldsFromRequest($request); + + $application->fill($request->all()); + + $dockerComposeDomainsJson = collect(); + if ($request->has('docker_compose_domains')) { + $yaml = Yaml::parse($application->docker_compose_raw); + $services = data_get($yaml, 'services'); + $dockerComposeDomains = collect($request->docker_compose_domains); + if ($dockerComposeDomains->count() > 0) { + $dockerComposeDomains->each(function ($domain, $key) use ($services, $dockerComposeDomainsJson) { + $name = data_get($domain, 'name'); + if (data_get($services, $name)) { + $dockerComposeDomainsJson->put($name, ['domain' => data_get($domain, 'domain')]); + } + }); + } + $request->offsetUnset('docker_compose_domains'); + } + if ($dockerComposeDomainsJson->count() > 0) { + $application->docker_compose_domains = $dockerComposeDomainsJson; + } + $application->fqdn = $fqdn; + $application->git_repository = $gitRepository; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + $application->source_type = $githubApp->getMorphClass(); + $application->source_id = $githubApp->id; + if (isset($useBuildServer)) { + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); + } + $application->save(); + $application->refresh(); + if (! $application->settings->is_container_label_readonly_enabled) { + $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); + $application->save(); + } + $application->isConfigurationChanged(true); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2; + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } else { + if ($application->build_pack === 'dockercompose') { + LoadComposeFile::dispatch($application); + } + } + + return response()->json(serializeApiResponse([ + 'uuid' => data_get($application, 'uuid'), + 'domains' => data_get($application, 'domains'), + ])); + } elseif ($type === 'private-deploy-key') { + if (! $request->has('name')) { + $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); + } + if ($request->build_pack === 'dockercompose') { + $request->offsetSet('ports_exposes', '80'); + } + + $validationRules = [ + 'git_repository' => 'string|required', + 'git_branch' => 'string|required', + 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)], + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', + 'private_key_uuid' => 'string|required', + 'watch_paths' => 'string|nullable', + 'docker_compose_location' => 'string', + 'docker_compose_raw' => 'string|nullable', + ]; + + $validationRules = array_merge($validationRules, sharedDataApplications()); + $validator = customApiValidator($request->all(), $validationRules); + + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $privateKey = PrivateKey::whereTeamId($teamId)->where('uuid', $request->private_key_uuid)->first(); + if (! $privateKey) { + return response()->json(['message' => 'Private Key not found.'], 404); + } + + $application = new Application; + removeUnnecessaryFieldsFromRequest($request); + + $application->fill($request->all()); + + $dockerComposeDomainsJson = collect(); + if ($request->has('docker_compose_domains')) { + $yaml = Yaml::parse($application->docker_compose_raw); + $services = data_get($yaml, 'services'); + $dockerComposeDomains = collect($request->docker_compose_domains); + if ($dockerComposeDomains->count() > 0) { + $dockerComposeDomains->each(function ($domain, $key) use ($services, $dockerComposeDomainsJson) { + $name = data_get($domain, 'name'); + if (data_get($services, $name)) { + $dockerComposeDomainsJson->put($name, ['domain' => data_get($domain, 'domain')]); + } + }); + } + $request->offsetUnset('docker_compose_domains'); + } + if ($dockerComposeDomainsJson->count() > 0) { + $application->docker_compose_domains = $dockerComposeDomainsJson; + } + $application->fqdn = $fqdn; + $application->private_key_id = $privateKey->id; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + if (isset($useBuildServer)) { + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); + } + $application->save(); + $application->refresh(); + if (! $application->settings->is_container_label_readonly_enabled) { + $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); + $application->save(); + } + $application->isConfigurationChanged(true); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2; + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } else { + if ($application->build_pack === 'dockercompose') { + LoadComposeFile::dispatch($application); + } + } + + return response()->json(serializeApiResponse([ + 'uuid' => data_get($application, 'uuid'), + 'domains' => data_get($application, 'domains'), + ])); + } elseif ($type === 'dockerfile') { + if (! $request->has('name')) { + $request->offsetSet('name', 'dockerfile-'.new Cuid2); + } + + $validationRules = [ + 'dockerfile' => 'string|required', + ]; + $validationRules = array_merge($validationRules, sharedDataApplications()); + $validator = customApiValidator($request->all(), $validationRules); + + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + if (! isBase64Encoded($request->dockerfile)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'dockerfile' => 'The dockerfile should be base64 encoded.', + ], + ], 422); + } + $dockerFile = base64_decode($request->dockerfile); + if (mb_detect_encoding($dockerFile, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'dockerfile' => 'The dockerfile should be base64 encoded.', + ], + ], 422); + } + $dockerFile = base64_decode($request->dockerfile); + removeUnnecessaryFieldsFromRequest($request); + + $port = get_port_from_dockerfile($request->dockerfile); + if (! $port) { + $port = 80; + } + + $application = new Application; + $application->fill($request->all()); + $application->fqdn = $fqdn; + $application->ports_exposes = $port; + $application->build_pack = 'dockerfile'; + $application->dockerfile = $dockerFile; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + if (isset($useBuildServer)) { + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); + } + + $application->git_repository = 'coollabsio/coolify'; + $application->git_branch = 'main'; + $application->save(); + $application->refresh(); + if (! $application->settings->is_container_label_readonly_enabled) { + $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); + $application->save(); + } + $application->isConfigurationChanged(true); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2; + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } + + return response()->json(serializeApiResponse([ + 'uuid' => data_get($application, 'uuid'), + 'domains' => data_get($application, 'domains'), + ])); + } elseif ($type === 'dockerimage') { + if (! $request->has('name')) { + $request->offsetSet('name', 'docker-image-'.new Cuid2); + } + $validationRules = [ + 'docker_registry_image_name' => 'string|required', + 'docker_registry_image_tag' => 'string', + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', + ]; + $validationRules = array_merge($validationRules, sharedDataApplications()); + $validator = customApiValidator($request->all(), $validationRules); + + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + if (! $request->docker_registry_image_tag) { + $request->offsetSet('docker_registry_image_tag', 'latest'); + } + $application = new Application; + removeUnnecessaryFieldsFromRequest($request); + + $application->fill($request->all()); + $application->fqdn = $fqdn; + $application->build_pack = 'dockerimage'; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + if (isset($useBuildServer)) { + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); + } + + $application->git_repository = 'coollabsio/coolify'; + $application->git_branch = 'main'; + $application->save(); + $application->refresh(); + if (! $application->settings->is_container_label_readonly_enabled) { + $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); + $application->save(); + } + $application->isConfigurationChanged(true); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2; + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } + + return response()->json(serializeApiResponse([ + 'uuid' => data_get($application, 'uuid'), + 'domains' => data_get($application, 'domains'), + ])); + } elseif ($type === 'dockercompose') { + $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'instant_deploy', 'docker_compose_raw']; + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + if (! $request->has('name')) { + $request->offsetSet('name', 'service'.new Cuid2); + } + $validationRules = [ + 'docker_compose_raw' => 'string|required', + ]; + $validationRules = array_merge($validationRules, sharedDataApplications()); + $validator = customApiValidator($request->all(), $validationRules); + + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + if (! isBase64Encoded($request->docker_compose_raw)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.', + ], + ], 422); + } + $dockerComposeRaw = base64_decode($request->docker_compose_raw); + if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.', + ], + ], 422); + } + $dockerCompose = base64_decode($request->docker_compose_raw); + $dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); + + // $isValid = validateComposeFile($dockerComposeRaw, $server_id); + // if ($isValid !== 'OK') { + // return $this->dispatch('error', "Invalid docker-compose file.\n$isValid"); + // } + + $service = new Service; + removeUnnecessaryFieldsFromRequest($request); + $service->fill($request->all()); + + $service->docker_compose_raw = $dockerComposeRaw; + $service->environment_id = $environment->id; + $service->server_id = $server->id; + $service->destination_id = $destination->id; + $service->destination_type = $destination->getMorphClass(); + $service->save(); + + $service->name = "service-$service->uuid"; + $service->parse(isNew: true); + if ($instantDeploy) { + StartService::dispatch($service)->onQueue('high'); + } + + return response()->json(serializeApiResponse([ + 'uuid' => data_get($service, 'uuid'), + 'domains' => data_get($service, 'domains'), + ])); + } + + return response()->json(['message' => 'Invalid type.'], 400); + } + + #[OA\Get( + summary: 'Get', + description: 'Get application by UUID.', + path: '/applications/{uuid}', + operationId: 'get-application-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Get application by UUID.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + ref: '#/components/schemas/Application' + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function application_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + if (! $application) { + return response()->json(['message' => 'Application not found.'], 404); + } + + return response()->json($this->removeSensitiveData($application)); + } + + #[OA\Delete( + summary: 'Delete', + description: 'Delete application by UUID.', + path: '/applications/{uuid}', + operationId: 'delete-application-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Application deleted.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Application deleted.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function delete_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + $cleanup = filter_var($request->query->get('cleanup', true), FILTER_VALIDATE_BOOLEAN); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['message' => 'UUID is required.'], 404); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'message' => 'Application not found', + ], 404); + } + + DeleteResourceJob::dispatch( + resource: $application, + deleteConfigurations: $request->query->get('delete_configurations', true), + deleteVolumes: $request->query->get('delete_volumes', true), + dockerCleanup: $request->query->get('docker_cleanup', true), + deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) + )->onQueue('high'); + + return response()->json([ + 'message' => 'Application deletion request queued.', + ]); + } + + #[OA\Patch( + summary: 'Update', + description: 'Update application by UUID.', + path: '/applications/{uuid}', + operationId: 'update-application-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + requestBody: new OA\RequestBody( + description: 'Application updated.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'], + 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'], + 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'], + 'github_app_uuid' => ['type' => 'string', 'description' => 'The Github App UUID.'], + 'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'], + 'git_branch' => ['type' => 'string', 'description' => 'The git branch.'], + 'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'], + 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'], + 'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'], + 'name' => ['type' => 'string', 'description' => 'The application name.'], + 'description' => ['type' => 'string', 'description' => 'The application description.'], + 'domains' => ['type' => 'string', 'description' => 'The application domains.'], + 'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'], + 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], + 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], + 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'], + 'install_command' => ['type' => 'string', 'description' => 'The install command.'], + 'build_command' => ['type' => 'string', 'description' => 'The build command.'], + 'start_command' => ['type' => 'string', 'description' => 'The start command.'], + 'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'], + 'base_directory' => ['type' => 'string', 'description' => 'The base directory for all commands.'], + 'publish_directory' => ['type' => 'string', 'description' => 'The publish directory.'], + 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'], + 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'], + 'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'], + 'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'], + 'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'], + 'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'], + 'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'], + 'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'], + 'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'], + 'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'], + 'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'], + 'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'], + 'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'], + 'custom_labels' => ['type' => 'string', 'description' => 'Custom labels.'], + 'custom_docker_run_options' => ['type' => 'string', 'description' => 'Custom docker run options.'], + 'post_deployment_command' => ['type' => 'string', 'description' => 'Post deployment command.'], + 'post_deployment_command_container' => ['type' => 'string', 'description' => 'Post deployment command container.'], + 'pre_deployment_command' => ['type' => 'string', 'description' => 'Pre deployment command.'], + 'pre_deployment_command_container' => ['type' => 'string', 'description' => 'Pre deployment command container.'], + 'manual_webhook_secret_github' => ['type' => 'string', 'description' => 'Manual webhook secret for Github.'], + 'manual_webhook_secret_gitlab' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitlab.'], + 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'description' => 'Manual webhook secret for Bitbucket.'], + 'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'], + 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], + 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'], + 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'], + 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'], + 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], + 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], + 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], + 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], + ], + )), + ]), + responses: [ + new OA\Response( + response: 200, + description: 'Application updated.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'uuid' => ['type' => 'string'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function update_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + if ($request->collect()->count() == 0) { + return response()->json([ + 'message' => 'Invalid request.', + ], 400); + } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'message' => 'Application not found', + ], 404); + } + $server = $application->destination->server; + $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration']; + + $validationRules = [ + 'name' => 'string|max:255', + 'description' => 'string|nullable', + 'static_image' => 'string', + 'watch_paths' => 'string|nullable', + 'docker_compose_location' => 'string', + 'docker_compose_raw' => 'string|nullable', + 'docker_compose_domains' => 'array|nullable', + 'docker_compose_custom_start_command' => 'string|nullable', + 'docker_compose_custom_build_command' => 'string|nullable', + 'custom_nginx_configuration' => 'string|nullable', + ]; + $validationRules = array_merge($validationRules, sharedDataApplications()); + $validator = customApiValidator($request->all(), $validationRules); + + // Validate ports_exposes + if ($request->has('ports_exposes')) { + $ports = explode(',', $request->ports_exposes); + foreach ($ports as $port) { + if (! is_numeric($port)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'ports_exposes' => 'The ports_exposes should be a comma separated list of numbers.', + ], + ], 422); + } + } + } + if ($request->has('custom_nginx_configuration')) { + if (! isBase64Encoded($request->custom_nginx_configuration)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.', + ], + ], 422); + } + $customNginxConfiguration = base64_decode($request->custom_nginx_configuration); + if (mb_detect_encoding($customNginxConfiguration, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.', + ], + ], 422); + } + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $domains = $request->domains; + if ($request->has('domains') && $server->isProxyShouldRun()) { + $errors = []; + $fqdn = $request->domains; + $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); + $fqdn = str($fqdn)->replaceStart(',', '')->trim(); + $application->fqdn = $fqdn; + if (! $application->settings->is_container_label_readonly_enabled) { + $customLabels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); + $application->custom_labels = base64_encode($customLabels); + } + $request->offsetUnset('domains'); + } + + $dockerComposeDomainsJson = collect(); + if ($request->has('docker_compose_domains')) { + $yaml = Yaml::parse($application->docker_compose_raw); + $services = data_get($yaml, 'services'); + $dockerComposeDomains = collect($request->docker_compose_domains); + if ($dockerComposeDomains->count() > 0) { + $dockerComposeDomains->each(function ($domain, $key) use ($services, $dockerComposeDomainsJson) { + $name = data_get($domain, 'name'); + if (data_get($services, $name)) { + $dockerComposeDomainsJson->put($name, ['domain' => data_get($domain, 'domain')]); + } + }); + } + $request->offsetUnset('docker_compose_domains'); + } + $instantDeploy = $request->instant_deploy; + $isStatic = $request->is_static; + $useBuildServer = $request->use_build_server; + + if (isset($useBuildServer)) { + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); + } + + if (isset($isStatic)) { + $application->settings->is_static = $isStatic; + $application->settings->save(); + } + + removeUnnecessaryFieldsFromRequest($request); + + $data = $request->all(); + data_set($data, 'fqdn', $domains); + if ($dockerComposeDomainsJson->count() > 0) { + data_set($data, 'docker_compose_domains', json_encode($dockerComposeDomainsJson)); + } + $application->fill($data); + $application->save(); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2; + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + is_api: true, + ); + } + + return response()->json([ + 'uuid' => $application->uuid, + ]); + } + + #[OA\Get( + summary: 'List Envs', + description: 'List all envs by application UUID.', + path: '/applications/{uuid}/envs', + operationId: 'list-envs-by-application-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'All environment variables by application UUID.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable') + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function envs(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'message' => 'Application not found', + ], 404); + } + $envs = $application->environment_variables->sortBy('id')->merge($application->environment_variables_preview->sortBy('id')); + + $envs = $envs->map(function ($env) { + $env->makeHidden([ + 'service_id', + 'standalone_clickhouse_id', + 'standalone_dragonfly_id', + 'standalone_keydb_id', + 'standalone_mariadb_id', + 'standalone_mongodb_id', + 'standalone_mysql_id', + 'standalone_postgresql_id', + 'standalone_redis_id', + ]); + + return $this->removeSensitiveData($env); + }); + + return response()->json($envs); + } + + #[OA\Patch( + summary: 'Update Env', + description: 'Update env by application UUID.', + path: '/applications/{uuid}/envs', + operationId: 'update-env-by-application-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + requestBody: new OA\RequestBody( + description: 'Env updated.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['key', 'value'], + properties: [ + 'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'], + 'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'], + 'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'], + 'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'], + 'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'], + 'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'], + 'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'], + ], + ), + ), + ], + ), + responses: [ + new OA\Response( + response: 201, + description: 'Environment variable updated.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Environment variable updated.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function update_env_by_uuid(Request $request) + { + $allowedFields = ['key', 'value', 'is_preview', 'is_build_time', 'is_literal']; + $teamId = getTeamIdFromToken(); + + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'message' => 'Application not found', + ], 404); + } + $validator = customApiValidator($request->all(), [ + 'key' => 'string|required', + 'value' => 'string|nullable', + 'is_preview' => 'boolean', + 'is_build_time' => 'boolean', + 'is_literal' => 'boolean', + 'is_multiline' => 'boolean', + 'is_shown_once' => 'boolean', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $is_preview = $request->is_preview ?? false; + $is_build_time = $request->is_build_time ?? false; + $is_literal = $request->is_literal ?? false; + if ($is_preview) { + $env = $application->environment_variables_preview->where('key', $request->key)->first(); + if ($env) { + $env->value = $request->value; + if ($env->is_build_time != $is_build_time) { + $env->is_build_time = $is_build_time; + } + if ($env->is_literal != $is_literal) { + $env->is_literal = $is_literal; + } + if ($env->is_preview != $is_preview) { + $env->is_preview = $is_preview; + } + if ($env->is_multiline != $request->is_multiline) { + $env->is_multiline = $request->is_multiline; + } + if ($env->is_shown_once != $request->is_shown_once) { + $env->is_shown_once = $request->is_shown_once; + } + $env->save(); + + return response()->json($this->removeSensitiveData($env))->setStatusCode(201); + } else { + return response()->json([ + 'message' => 'Environment variable not found.', + ], 404); + } + } else { + $env = $application->environment_variables->where('key', $request->key)->first(); + if ($env) { + $env->value = $request->value; + if ($env->is_build_time != $is_build_time) { + $env->is_build_time = $is_build_time; + } + if ($env->is_literal != $is_literal) { + $env->is_literal = $is_literal; + } + if ($env->is_preview != $is_preview) { + $env->is_preview = $is_preview; + } + if ($env->is_multiline != $request->is_multiline) { + $env->is_multiline = $request->is_multiline; + } + if ($env->is_shown_once != $request->is_shown_once) { + $env->is_shown_once = $request->is_shown_once; + } + $env->save(); + + return response()->json($this->removeSensitiveData($env))->setStatusCode(201); + } else { + return response()->json([ + 'message' => 'Environment variable not found.', + ], 404); + } + } + + return response()->json([ + 'message' => 'Something is not okay. Are you okay?', + ], 500); + } + + #[OA\Patch( + summary: 'Update Envs (Bulk)', + description: 'Update multiple envs by application UUID.', + path: '/applications/{uuid}/envs/bulk', + operationId: 'update-envs-by-application-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + requestBody: new OA\RequestBody( + description: 'Bulk envs updated.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['data'], + properties: [ + 'data' => [ + 'type' => 'array', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'], + 'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'], + 'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'], + 'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'], + 'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'], + 'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'], + 'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'], + ], + ), + ], + ], + ), + ), + ], + ), + responses: [ + new OA\Response( + response: 201, + description: 'Environment variables updated.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Environment variables updated.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function create_bulk_envs(Request $request) + { + $teamId = getTeamIdFromToken(); + + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'message' => 'Application not found', + ], 404); + } + + $bulk_data = $request->get('data'); + if (! $bulk_data) { + return response()->json([ + 'message' => 'Bulk data is required.', + ], 400); + } + $bulk_data = collect($bulk_data)->map(function ($item) { + return collect($item)->only(['key', 'value', 'is_preview', 'is_build_time', 'is_literal']); + }); + foreach ($bulk_data as $item) { + $validator = customApiValidator($item, [ + 'key' => 'string|required', + 'value' => 'string|nullable', + 'is_preview' => 'boolean', + 'is_build_time' => 'boolean', + 'is_literal' => 'boolean', + 'is_multiline' => 'boolean', + 'is_shown_once' => 'boolean', + ]); + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $is_preview = $item->get('is_preview') ?? false; + $is_build_time = $item->get('is_build_time') ?? false; + $is_literal = $item->get('is_literal') ?? false; + $is_multi_line = $item->get('is_multiline') ?? false; + $is_shown_once = $item->get('is_shown_once') ?? false; + if ($is_preview) { + $env = $application->environment_variables_preview->where('key', $item->get('key'))->first(); + if ($env) { + $env->value = $item->get('value'); + if ($env->is_build_time != $is_build_time) { + $env->is_build_time = $is_build_time; + } + if ($env->is_literal != $is_literal) { + $env->is_literal = $is_literal; + } + if ($env->is_multiline != $item->get('is_multiline')) { + $env->is_multiline = $item->get('is_multiline'); + } + if ($env->is_shown_once != $item->get('is_shown_once')) { + $env->is_shown_once = $item->get('is_shown_once'); + } + $env->save(); + } else { + $env = $application->environment_variables()->create([ + 'key' => $item->get('key'), + 'value' => $item->get('value'), + 'is_preview' => $is_preview, + 'is_build_time' => $is_build_time, + 'is_literal' => $is_literal, + 'is_multiline' => $is_multi_line, + 'is_shown_once' => $is_shown_once, + ]); + } + } else { + $env = $application->environment_variables->where('key', $item->get('key'))->first(); + if ($env) { + $env->value = $item->get('value'); + if ($env->is_build_time != $is_build_time) { + $env->is_build_time = $is_build_time; + } + if ($env->is_literal != $is_literal) { + $env->is_literal = $is_literal; + } + if ($env->is_multiline != $item->get('is_multiline')) { + $env->is_multiline = $item->get('is_multiline'); + } + if ($env->is_shown_once != $item->get('is_shown_once')) { + $env->is_shown_once = $item->get('is_shown_once'); + } + $env->save(); + } else { + $env = $application->environment_variables()->create([ + 'key' => $item->get('key'), + 'value' => $item->get('value'), + 'is_preview' => $is_preview, + 'is_build_time' => $is_build_time, + 'is_literal' => $is_literal, + 'is_multiline' => $is_multi_line, + 'is_shown_once' => $is_shown_once, + ]); + } + } + } + + return response()->json($this->removeSensitiveData($env))->setStatusCode(201); + } + + #[OA\Post( + summary: 'Create Env', + description: 'Create env by application UUID.', + path: '/applications/{uuid}/envs', + operationId: 'create-env-by-application-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + requestBody: new OA\RequestBody( + required: true, + description: 'Env created.', + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'], + 'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'], + 'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'], + 'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'], + 'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'], + 'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'], + 'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'], + ], + ), + ), + ), + responses: [ + new OA\Response( + response: 201, + description: 'Environment variable created.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'uuid' => ['type' => 'string', 'example' => 'nc0k04gk8g0cgsk440g0koko'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function create_env(Request $request) + { + $allowedFields = ['key', 'value', 'is_preview', 'is_build_time', 'is_literal']; + $teamId = getTeamIdFromToken(); + + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'message' => 'Application not found', + ], 404); + } + $validator = customApiValidator($request->all(), [ + 'key' => 'string|required', + 'value' => 'string|nullable', + 'is_preview' => 'boolean', + 'is_build_time' => 'boolean', + 'is_literal' => 'boolean', + 'is_multiline' => 'boolean', + 'is_shown_once' => 'boolean', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $is_preview = $request->is_preview ?? false; + if ($is_preview) { + $env = $application->environment_variables_preview->where('key', $request->key)->first(); + if ($env) { + return response()->json([ + 'message' => 'Environment variable already exists. Use PATCH request to update it.', + ], 409); + } else { + $env = $application->environment_variables()->create([ + 'key' => $request->key, + 'value' => $request->value, + 'is_preview' => $request->is_preview ?? false, + 'is_build_time' => $request->is_build_time ?? false, + 'is_literal' => $request->is_literal ?? false, + 'is_multiline' => $request->is_multiline ?? false, + 'is_shown_once' => $request->is_shown_once ?? false, + ]); + + return response()->json([ + 'uuid' => $env->uuid, + ])->setStatusCode(201); + } + } else { + $env = $application->environment_variables->where('key', $request->key)->first(); + if ($env) { + return response()->json([ + 'message' => 'Environment variable already exists. Use PATCH request to update it.', + ], 409); + } else { + $env = $application->environment_variables()->create([ + 'key' => $request->key, + 'value' => $request->value, + 'is_preview' => $request->is_preview ?? false, + 'is_build_time' => $request->is_build_time ?? false, + 'is_literal' => $request->is_literal ?? false, + 'is_multiline' => $request->is_multiline ?? false, + 'is_shown_once' => $request->is_shown_once ?? false, + ]); + + return response()->json([ + 'uuid' => $env->uuid, + ])->setStatusCode(201); + } + } + + return response()->json([ + 'message' => 'Something went wrong.', + ], 500); + } + + #[OA\Delete( + summary: 'Delete Env', + description: 'Delete env by UUID.', + path: '/applications/{uuid}/envs/{env_uuid}', + operationId: 'delete-env-by-application-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + new OA\Parameter( + name: 'env_uuid', + in: 'path', + description: 'UUID of the environment variable.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Environment variable deleted.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Environment variable deleted.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function delete_env_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'message' => 'Application not found.', + ], 404); + } + $found_env = EnvironmentVariable::where('uuid', $request->env_uuid)->where('application_id', $application->id)->first(); + if (! $found_env) { + return response()->json([ + 'message' => 'Environment variable not found.', + ], 404); + } + $found_env->forceDelete(); + + return response()->json([ + 'message' => 'Environment variable deleted.', + ]); + } + + #[OA\Get( + summary: 'Start', + description: 'Start application. `Post` request is also accepted.', + path: '/applications/{uuid}/start', + operationId: 'start-application-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + new OA\Parameter( + name: 'force', + in: 'query', + description: 'Force rebuild.', + schema: new OA\Schema( + type: 'boolean', + default: false, + ) + ), + new OA\Parameter( + name: 'instant_deploy', + in: 'query', + description: 'Instant deploy (skip queuing).', + schema: new OA\Schema( + type: 'boolean', + default: false, + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Start application.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Deployment request queued.', 'description' => 'Message.'], + 'deployment_uuid' => ['type' => 'string', 'example' => 'doogksw', 'description' => 'UUID of the deployment.'], + ]) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function action_deploy(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $force = $request->query->get('force') ?? false; + $instant_deploy = $request->query->get('instant_deploy') ?? false; + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + if (! $application) { + return response()->json(['message' => 'Application not found.'], 404); + } + + $deployment_uuid = new Cuid2; + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + force_rebuild: $force, + is_api: true, + no_questions_asked: $instant_deploy + ); + + return response()->json( + [ + 'message' => 'Deployment request queued.', + 'deployment_uuid' => $deployment_uuid->toString(), + ], + 200 + ); + } + + #[OA\Get( + summary: 'Stop', + description: 'Stop application. `Post` request is also accepted.', + path: '/applications/{uuid}/stop', + operationId: 'stop-application-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Stop application.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Application stopping request queued.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function action_stop(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + if (! $application) { + return response()->json(['message' => 'Application not found.'], 404); + } + StopApplication::dispatch($application)->onQueue('high'); + + return response()->json( + [ + 'message' => 'Application stopping request queued.', + ], + ); + } + + #[OA\Get( + summary: 'Restart', + description: 'Restart application. `Post` request is also accepted.', + path: '/applications/{uuid}/restart', + operationId: 'restart-application-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Restart application.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Restart request queued.'], + 'deployment_uuid' => ['type' => 'string', 'example' => 'doogksw', 'description' => 'UUID of the deployment.'], + ] + ) + ), + ]), + + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function action_restart(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + if (! $application) { + return response()->json(['message' => 'Application not found.'], 404); + } + + $deployment_uuid = new Cuid2; + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + restart_only: true, + is_api: true, + ); + + return response()->json( + [ + 'message' => 'Restart request queued.', + 'deployment_uuid' => $deployment_uuid->toString(), + ], + ); + } + + #[OA\Post( + summary: 'Execute Command', + description: "Execute a command on the application's current container.", + path: '/applications/{uuid}/execute', + operationId: 'execute-command-application', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + requestBody: new OA\RequestBody( + required: true, + description: 'Command to execute.', + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'command' => ['type' => 'string', 'description' => 'Command to execute.'], + ], + ), + ), + ), + responses: [ + new OA\Response( + response: 200, + description: "Execute a command on the application's current container.", + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Command executed.'], + 'response' => ['type' => 'string'], + ] + ) + ), + ] + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function execute_command_by_uuid(Request $request) + { + // TODO: Need to review this from security perspective, to not allow arbitrary command execution + $allowedFields = ['command']; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + if (! $application) { + return response()->json(['message' => 'Application not found.'], 404); + } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'command' => 'string|required', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + + $container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail(); + $status = getContainerStatus($application->destination->server, $container['Names']); + + if ($status !== 'running') { + return response()->json([ + 'message' => 'Application is not running.', + ], 400); + } + + $commands = collect([ + executeInDocker($container['Names'], $request->command), + ]); + + $res = instant_remote_process(command: $commands, server: $application->destination->server); + + return response()->json([ + 'message' => 'Command executed.', + 'response' => $res, + ]); + } + + private function validateDataApplications(Request $request, Server $server) + { + $teamId = getTeamIdFromToken(); + + // Validate ports_mappings + if ($request->has('ports_mappings')) { + $ports = []; + foreach (explode(',', $request->ports_mappings) as $portMapping) { + $port = explode(':', $portMapping); + if (in_array($port[0], $ports)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'ports_mappings' => 'The first number before : should be unique between mappings.', + ], + ], 422); + } + $ports[] = $port[0]; + } + } + // Validate custom_labels + if ($request->has('custom_labels')) { + if (! isBase64Encoded($request->custom_labels)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'custom_labels' => 'The custom_labels should be base64 encoded.', + ], + ], 422); + } + $customLabels = base64_decode($request->custom_labels); + if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'custom_labels' => 'The custom_labels should be base64 encoded.', + ], + ], 422); + } + } + if ($request->has('domains') && $server->isProxyShouldRun()) { + $uuid = $request->uuid; + $fqdn = $request->domains; + $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); + $fqdn = str($fqdn)->replaceStart(',', '')->trim(); + $errors = []; + $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) { + if (filter_var($domain, FILTER_VALIDATE_URL) === false) { + $errors[] = 'Invalid domain: '.$domain; + } + + return str($domain)->trim()->lower(); + }); + if (count($errors) > 0) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'domains' => 'One of the domain is already used.', + ], + ], 422); + } + } + } +} diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php new file mode 100644 index 000000000..eaa542a83 --- /dev/null +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -0,0 +1,1827 @@ +user()->currentAccessToken(); + $database->makeHidden([ + 'id', + 'laravel_through_key', + ]); + if ($token->can('view:sensitive')) { + return serializeApiResponse($database); + } + + $database->makeHidden([ + 'internal_db_url', + 'external_db_url', + 'postgres_password', + 'dragonfly_password', + 'redis_password', + 'mongo_initdb_root_password', + 'keydb_password', + 'clickhouse_admin_password', + ]); + + return serializeApiResponse($database); + } + + #[OA\Get( + summary: 'List', + description: 'List all databases.', + path: '/databases', + operationId: 'list-databases', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + responses: [ + new OA\Response( + response: 200, + description: 'Get all databases', + content: new OA\JsonContent( + type: 'string', + example: 'Content is very complex. Will be implemented later.', + ), + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function databases(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $projects = Project::where('team_id', $teamId)->get(); + $databases = collect(); + foreach ($projects as $project) { + $databases = $databases->merge($project->databases()); + } + $databases = $databases->map(function ($database) { + return $this->removeSensitiveData($database); + }); + + return response()->json($databases); + } + + #[OA\Get( + summary: 'Get', + description: 'Get database by UUID.', + path: '/databases/{uuid}', + operationId: 'get-database-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the database.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Get all databases', + content: new OA\JsonContent( + type: 'string', + example: 'Content is very complex. Will be implemented later.', + ), + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function database_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['message' => 'UUID is required.'], 404); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + + return response()->json($this->removeSensitiveData($database)); + } + + #[OA\Patch( + summary: 'Update', + description: 'Update database by UUID.', + path: '/databases/{uuid}', + operationId: 'update-database-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the database.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + requestBody: new OA\RequestBody( + description: 'Database data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'name' => ['type' => 'string', 'description' => 'Name of the database'], + 'description' => ['type' => 'string', 'description' => 'Description of the database'], + 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], + 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], + 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'], + 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'], + 'postgres_user' => ['type' => 'string', 'description' => 'PostgreSQL user'], + 'postgres_password' => ['type' => 'string', 'description' => 'PostgreSQL password'], + 'postgres_db' => ['type' => 'string', 'description' => 'PostgreSQL database'], + 'postgres_initdb_args' => ['type' => 'string', 'description' => 'PostgreSQL initdb args'], + 'postgres_host_auth_method' => ['type' => 'string', 'description' => 'PostgreSQL host auth method'], + 'postgres_conf' => ['type' => 'string', 'description' => 'PostgreSQL conf'], + 'clickhouse_admin_user' => ['type' => 'string', 'description' => 'Clickhouse admin user'], + 'clickhouse_admin_password' => ['type' => 'string', 'description' => 'Clickhouse admin password'], + 'dragonfly_password' => ['type' => 'string', 'description' => 'DragonFly password'], + 'redis_password' => ['type' => 'string', 'description' => 'Redis password'], + 'redis_conf' => ['type' => 'string', 'description' => 'Redis conf'], + 'keydb_password' => ['type' => 'string', 'description' => 'KeyDB password'], + 'keydb_conf' => ['type' => 'string', 'description' => 'KeyDB conf'], + 'mariadb_conf' => ['type' => 'string', 'description' => 'MariaDB conf'], + 'mariadb_root_password' => ['type' => 'string', 'description' => 'MariaDB root password'], + 'mariadb_user' => ['type' => 'string', 'description' => 'MariaDB user'], + 'mariadb_password' => ['type' => 'string', 'description' => 'MariaDB password'], + 'mariadb_database' => ['type' => 'string', 'description' => 'MariaDB database'], + 'mongo_conf' => ['type' => 'string', 'description' => 'Mongo conf'], + 'mongo_initdb_root_username' => ['type' => 'string', 'description' => 'Mongo initdb root username'], + 'mongo_initdb_root_password' => ['type' => 'string', 'description' => 'Mongo initdb root password'], + 'mongo_initdb_init_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'], + 'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'], + 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], + 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'], + 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database updated', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function update_by_uuid(Request $request) + { + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf']; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255', + 'description' => 'string|nullable', + 'image' => 'string', + 'is_public' => 'boolean', + 'public_port' => 'numeric|nullable', + 'limits_memory' => 'string', + 'limits_memory_swap' => 'string', + 'limits_memory_swappiness' => 'numeric', + 'limits_memory_reservation' => 'string', + 'limits_cpus' => 'string', + 'limits_cpuset' => 'string|nullable', + 'limits_cpu_shares' => 'numeric', + ]); + + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $uuid = $request->uuid; + removeUnnecessaryFieldsFromRequest($request); + $database = queryDatabaseByUuidWithinTeam($uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + if ($request->is_public && $request->public_port) { + if (isPublicPortAlreadyUsed($database->destination->server, $request->public_port, $database->id)) { + return response()->json(['message' => 'Public port already used by another database.'], 400); + } + } + switch ($database->type()) { + case 'standalone-postgresql': + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf']; + $validator = customApiValidator($request->all(), [ + 'postgres_user' => 'string', + 'postgres_password' => 'string', + 'postgres_db' => 'string', + 'postgres_initdb_args' => 'string', + 'postgres_host_auth_method' => 'string', + 'postgres_conf' => 'string', + ]); + if ($request->has('postgres_conf')) { + if (! isBase64Encoded($request->postgres_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'postgres_conf' => 'The postgres_conf should be base64 encoded.', + ], + ], 422); + } + $postgresConf = base64_decode($request->postgres_conf); + if (mb_detect_encoding($postgresConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'postgres_conf' => 'The postgres_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('postgres_conf', $postgresConf); + } + break; + case 'standalone-clickhouse': + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password']; + $validator = customApiValidator($request->all(), [ + 'clickhouse_admin_user' => 'string', + 'clickhouse_admin_password' => 'string', + ]); + break; + case 'standalone-dragonfly': + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password']; + $validator = customApiValidator($request->all(), [ + 'dragonfly_password' => 'string', + ]); + break; + case 'standalone-redis': + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf']; + $validator = customApiValidator($request->all(), [ + 'redis_password' => 'string', + 'redis_conf' => 'string', + ]); + if ($request->has('redis_conf')) { + if (! isBase64Encoded($request->redis_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'redis_conf' => 'The redis_conf should be base64 encoded.', + ], + ], 422); + } + $redisConf = base64_decode($request->redis_conf); + if (mb_detect_encoding($redisConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'redis_conf' => 'The redis_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('redis_conf', $redisConf); + } + break; + case 'standalone-keydb': + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf']; + $validator = customApiValidator($request->all(), [ + 'keydb_password' => 'string', + 'keydb_conf' => 'string', + ]); + if ($request->has('keydb_conf')) { + if (! isBase64Encoded($request->keydb_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'keydb_conf' => 'The keydb_conf should be base64 encoded.', + ], + ], 422); + } + $keydbConf = base64_decode($request->keydb_conf); + if (mb_detect_encoding($keydbConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'keydb_conf' => 'The keydb_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('keydb_conf', $keydbConf); + } + break; + case 'standalone-mariadb': + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database']; + $validator = customApiValidator($request->all(), [ + 'mariadb_conf' => 'string', + 'mariadb_root_password' => 'string', + 'mariadb_user' => 'string', + 'mariadb_password' => 'string', + 'mariadb_database' => 'string', + ]); + if ($request->has('mariadb_conf')) { + if (! isBase64Encoded($request->mariadb_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mariadb_conf' => 'The mariadb_conf should be base64 encoded.', + ], + ], 422); + } + $mariadbConf = base64_decode($request->mariadb_conf); + if (mb_detect_encoding($mariadbConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mariadb_conf' => 'The mariadb_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('mariadb_conf', $mariadbConf); + } + break; + case 'standalone-mongodb': + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database']; + $validator = customApiValidator($request->all(), [ + 'mongo_conf' => 'string', + 'mongo_initdb_root_username' => 'string', + 'mongo_initdb_root_password' => 'string', + 'mongo_initdb_init_database' => 'string', + ]); + if ($request->has('mongo_conf')) { + if (! isBase64Encoded($request->mongo_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mongo_conf' => 'The mongo_conf should be base64 encoded.', + ], + ], 422); + } + $mongoConf = base64_decode($request->mongo_conf); + if (mb_detect_encoding($mongoConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mongo_conf' => 'The mongo_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('mongo_conf', $mongoConf); + } + + break; + case 'standalone-mysql': + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf']; + $validator = customApiValidator($request->all(), [ + 'mysql_root_password' => 'string', + 'mysql_user' => 'string', + 'mysql_database' => 'string', + 'mysql_conf' => 'string', + ]); + if ($request->has('mysql_conf')) { + if (! isBase64Encoded($request->mysql_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mysql_conf' => 'The mysql_conf should be base64 encoded.', + ], + ], 422); + } + $mysqlConf = base64_decode($request->mysql_conf); + if (mb_detect_encoding($mysqlConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mysql_conf' => 'The mysql_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('mysql_conf', $mysqlConf); + } + break; + } + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $whatToDoWithDatabaseProxy = null; + if ($request->is_public === false && $database->is_public === true) { + $whatToDoWithDatabaseProxy = 'stop'; + } + if ($request->is_public === true && $request->public_port && $database->is_public === false) { + $whatToDoWithDatabaseProxy = 'start'; + } + + $database->update($request->all()); + + if ($whatToDoWithDatabaseProxy === 'start') { + StartDatabaseProxy::dispatch($database)->onQueue('high'); + } elseif ($whatToDoWithDatabaseProxy === 'stop') { + StopDatabaseProxy::dispatch($database)->onQueue('high'); + } + + return response()->json([ + 'message' => 'Database updated.', + ]); + } + + #[OA\Post( + summary: 'Create (PostgreSQL)', + description: 'Create a new PostgreSQL database.', + path: '/databases/postgresql', + operationId: 'create-database-postgresql', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + + requestBody: new OA\RequestBody( + description: 'Database data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['server_uuid', 'project_uuid', 'environment_name'], + properties: [ + 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], + 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], + 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], + 'postgres_user' => ['type' => 'string', 'description' => 'PostgreSQL user'], + 'postgres_password' => ['type' => 'string', 'description' => 'PostgreSQL password'], + 'postgres_db' => ['type' => 'string', 'description' => 'PostgreSQL database'], + 'postgres_initdb_args' => ['type' => 'string', 'description' => 'PostgreSQL initdb args'], + 'postgres_host_auth_method' => ['type' => 'string', 'description' => 'PostgreSQL host auth method'], + 'postgres_conf' => ['type' => 'string', 'description' => 'PostgreSQL conf'], + 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], + 'name' => ['type' => 'string', 'description' => 'Name of the database'], + 'description' => ['type' => 'string', 'description' => 'Description of the database'], + 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], + 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], + 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'], + 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database updated', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_database_postgresql(Request $request) + { + return $this->create_database($request, NewDatabaseTypes::POSTGRESQL); + } + + #[OA\Post( + summary: 'Create (Clickhouse)', + description: 'Create a new Clickhouse database.', + path: '/databases/clickhouse', + operationId: 'create-database-clickhouse', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + + requestBody: new OA\RequestBody( + description: 'Database data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['server_uuid', 'project_uuid', 'environment_name'], + properties: [ + 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], + 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], + 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], + 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], + 'clickhouse_admin_user' => ['type' => 'string', 'description' => 'Clickhouse admin user'], + 'clickhouse_admin_password' => ['type' => 'string', 'description' => 'Clickhouse admin password'], + 'name' => ['type' => 'string', 'description' => 'Name of the database'], + 'description' => ['type' => 'string', 'description' => 'Description of the database'], + 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], + 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], + 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'], + 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database updated', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_database_clickhouse(Request $request) + { + return $this->create_database($request, NewDatabaseTypes::CLICKHOUSE); + } + + #[OA\Post( + summary: 'Create (DragonFly)', + description: 'Create a new DragonFly database.', + path: '/databases/dragonfly', + operationId: 'create-database-dragonfly', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + + requestBody: new OA\RequestBody( + description: 'Database data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['server_uuid', 'project_uuid', 'environment_name'], + properties: [ + 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], + 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], + 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], + 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], + 'dragonfly_password' => ['type' => 'string', 'description' => 'DragonFly password'], + 'name' => ['type' => 'string', 'description' => 'Name of the database'], + 'description' => ['type' => 'string', 'description' => 'Description of the database'], + 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], + 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], + 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'], + 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database updated', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_database_dragonfly(Request $request) + { + return $this->create_database($request, NewDatabaseTypes::DRAGONFLY); + } + + #[OA\Post( + summary: 'Create (Redis)', + description: 'Create a new Redis database.', + path: '/databases/redis', + operationId: 'create-database-redis', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + + requestBody: new OA\RequestBody( + description: 'Database data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['server_uuid', 'project_uuid', 'environment_name'], + properties: [ + 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], + 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], + 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], + 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], + 'redis_password' => ['type' => 'string', 'description' => 'Redis password'], + 'redis_conf' => ['type' => 'string', 'description' => 'Redis conf'], + 'name' => ['type' => 'string', 'description' => 'Name of the database'], + 'description' => ['type' => 'string', 'description' => 'Description of the database'], + 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], + 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], + 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'], + 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database updated', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_database_redis(Request $request) + { + return $this->create_database($request, NewDatabaseTypes::REDIS); + } + + #[OA\Post( + summary: 'Create (KeyDB)', + description: 'Create a new KeyDB database.', + path: '/databases/keydb', + operationId: 'create-database-keydb', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + + requestBody: new OA\RequestBody( + description: 'Database data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['server_uuid', 'project_uuid', 'environment_name'], + properties: [ + 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], + 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], + 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], + 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], + 'keydb_password' => ['type' => 'string', 'description' => 'KeyDB password'], + 'keydb_conf' => ['type' => 'string', 'description' => 'KeyDB conf'], + 'name' => ['type' => 'string', 'description' => 'Name of the database'], + 'description' => ['type' => 'string', 'description' => 'Description of the database'], + 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], + 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], + 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'], + 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database updated', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_database_keydb(Request $request) + { + return $this->create_database($request, NewDatabaseTypes::KEYDB); + } + + #[OA\Post( + summary: 'Create (MariaDB)', + description: 'Create a new MariaDB database.', + path: '/databases/mariadb', + operationId: 'create-database-mariadb', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + + requestBody: new OA\RequestBody( + description: 'Database data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['server_uuid', 'project_uuid', 'environment_name'], + properties: [ + 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], + 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], + 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], + 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], + 'mariadb_conf' => ['type' => 'string', 'description' => 'MariaDB conf'], + 'mariadb_root_password' => ['type' => 'string', 'description' => 'MariaDB root password'], + 'mariadb_user' => ['type' => 'string', 'description' => 'MariaDB user'], + 'mariadb_password' => ['type' => 'string', 'description' => 'MariaDB password'], + 'mariadb_database' => ['type' => 'string', 'description' => 'MariaDB database'], + 'name' => ['type' => 'string', 'description' => 'Name of the database'], + 'description' => ['type' => 'string', 'description' => 'Description of the database'], + 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], + 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], + 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'], + 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database updated', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_database_mariadb(Request $request) + { + return $this->create_database($request, NewDatabaseTypes::MARIADB); + } + + #[OA\Post( + summary: 'Create (MySQL)', + description: 'Create a new MySQL database.', + path: '/databases/mysql', + operationId: 'create-database-mysql', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + + requestBody: new OA\RequestBody( + description: 'Database data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['server_uuid', 'project_uuid', 'environment_name'], + properties: [ + 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], + 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], + 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], + 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], + 'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'], + 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], + 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'], + 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'], + 'name' => ['type' => 'string', 'description' => 'Name of the database'], + 'description' => ['type' => 'string', 'description' => 'Description of the database'], + 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], + 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], + 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'], + 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database updated', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_database_mysql(Request $request) + { + return $this->create_database($request, NewDatabaseTypes::MYSQL); + } + + #[OA\Post( + summary: 'Create (MongoDB)', + description: 'Create a new MongoDB database.', + path: '/databases/mongodb', + operationId: 'create-database-mongodb', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + + requestBody: new OA\RequestBody( + description: 'Database data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['server_uuid', 'project_uuid', 'environment_name'], + properties: [ + 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], + 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], + 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], + 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], + 'mongo_conf' => ['type' => 'string', 'description' => 'MongoDB conf'], + 'mongo_initdb_root_username' => ['type' => 'string', 'description' => 'MongoDB initdb root username'], + 'name' => ['type' => 'string', 'description' => 'Name of the database'], + 'description' => ['type' => 'string', 'description' => 'Description of the database'], + 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], + 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], + 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'], + 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database updated', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_database_mongodb(Request $request) + { + return $this->create_database($request, NewDatabaseTypes::MONGODB); + } + + public function create_database(Request $request, NewDatabaseTypes $type) + { + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf']; + + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if (! empty($extraFields)) { + $errors = collect([]); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $serverUuid = $request->server_uuid; + $instantDeploy = $request->instant_deploy ?? false; + if ($request->is_public && ! $request->public_port) { + $request->offsetSet('is_public', false); + } + $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); + if (! $project) { + return response()->json(['message' => 'Project not found.'], 404); + } + $environment = $project->environments()->where('name', $request->environment_name)->first(); + if (! $environment) { + return response()->json(['message' => 'Environment not found.'], 404); + } + $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first(); + if (! $server) { + return response()->json(['message' => 'Server not found.'], 404); + } + $destinations = $server->destinations(); + if ($destinations->count() == 0) { + return response()->json(['message' => 'Server has no destinations.'], 400); + } + if ($destinations->count() > 1 && ! $request->has('destination_uuid')) { + return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400); + } + $destination = $destinations->first(); + if ($request->has('public_port') && $request->is_public) { + if (isPublicPortAlreadyUsed($server, $request->public_port)) { + return response()->json(['message' => 'Public port already used by another database.'], 400); + } + } + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255', + 'description' => 'string|nullable', + 'image' => 'string', + 'project_uuid' => 'string|required', + 'environment_name' => 'string|required', + 'server_uuid' => 'string|required', + 'destination_uuid' => 'string', + 'is_public' => 'boolean', + 'public_port' => 'numeric|nullable', + 'limits_memory' => 'string', + 'limits_memory_swap' => 'string', + 'limits_memory_swappiness' => 'numeric', + 'limits_memory_reservation' => 'string', + 'limits_cpus' => 'string', + 'limits_cpuset' => 'string|nullable', + 'limits_cpu_shares' => 'numeric', + 'instant_deploy' => 'boolean', + ]); + if ($validator->failed()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + if ($request->public_port) { + if ($request->public_port < 1024 || $request->public_port > 65535) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'public_port' => 'The public port should be between 1024 and 65535.', + ], + ], 422); + } + } + if ($type === NewDatabaseTypes::POSTGRESQL) { + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf']; + $validator = customApiValidator($request->all(), [ + 'postgres_user' => 'string', + 'postgres_password' => 'string', + 'postgres_db' => 'string', + 'postgres_initdb_args' => 'string', + 'postgres_host_auth_method' => 'string', + 'postgres_conf' => 'string', + ]); + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + removeUnnecessaryFieldsFromRequest($request); + if ($request->has('postgres_conf')) { + if (! isBase64Encoded($request->postgres_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'postgres_conf' => 'The postgres_conf should be base64 encoded.', + ], + ], 422); + } + $postgresConf = base64_decode($request->postgres_conf); + if (mb_detect_encoding($postgresConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'postgres_conf' => 'The postgres_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('postgres_conf', $postgresConf); + } + $database = create_standalone_postgresql($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartDatabase::dispatch($database)->onQueue('high'); + } + $database->refresh(); + $payload = [ + 'uuid' => $database->uuid, + 'internal_db_url' => $database->internal_db_url, + ]; + if ($database->is_public && $database->public_port) { + $payload['external_db_url'] = $database->external_db_url; + } + + return response()->json(serializeApiResponse($payload))->setStatusCode(201); + } elseif ($type === NewDatabaseTypes::MARIADB) { + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database']; + $validator = customApiValidator($request->all(), [ + 'clickhouse_admin_user' => 'string', + 'clickhouse_admin_password' => 'string', + ]); + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + removeUnnecessaryFieldsFromRequest($request); + if ($request->has('mariadb_conf')) { + if (! isBase64Encoded($request->mariadb_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mariadb_conf' => 'The mariadb_conf should be base64 encoded.', + ], + ], 422); + } + $mariadbConf = base64_decode($request->mariadb_conf); + if (mb_detect_encoding($mariadbConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mariadb_conf' => 'The mariadb_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('mariadb_conf', $mariadbConf); + } + $database = create_standalone_mariadb($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartDatabase::dispatch($database)->onQueue('high'); + } + + $database->refresh(); + $payload = [ + 'uuid' => $database->uuid, + 'internal_db_url' => $database->internal_db_url, + ]; + if ($database->is_public && $database->public_port) { + $payload['external_db_url'] = $database->external_db_url; + } + + return response()->json(serializeApiResponse($payload))->setStatusCode(201); + } elseif ($type === NewDatabaseTypes::MYSQL) { + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_user', 'mysql_database', 'mysql_conf']; + $validator = customApiValidator($request->all(), [ + 'mysql_root_password' => 'string', + 'mysql_user' => 'string', + 'mysql_database' => 'string', + 'mysql_conf' => 'string', + ]); + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + removeUnnecessaryFieldsFromRequest($request); + if ($request->has('mysql_conf')) { + if (! isBase64Encoded($request->mysql_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mysql_conf' => 'The mysql_conf should be base64 encoded.', + ], + ], 422); + } + $mysqlConf = base64_decode($request->mysql_conf); + if (mb_detect_encoding($mysqlConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mysql_conf' => 'The mysql_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('mysql_conf', $mysqlConf); + } + $database = create_standalone_mysql($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartDatabase::dispatch($database)->onQueue('high'); + } + + $database->refresh(); + $payload = [ + 'uuid' => $database->uuid, + 'internal_db_url' => $database->internal_db_url, + ]; + if ($database->is_public && $database->public_port) { + $payload['external_db_url'] = $database->external_db_url; + } + + return response()->json(serializeApiResponse($payload))->setStatusCode(201); + } elseif ($type === NewDatabaseTypes::REDIS) { + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf']; + $validator = customApiValidator($request->all(), [ + 'redis_password' => 'string', + 'redis_conf' => 'string', + ]); + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + removeUnnecessaryFieldsFromRequest($request); + if ($request->has('redis_conf')) { + if (! isBase64Encoded($request->redis_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'redis_conf' => 'The redis_conf should be base64 encoded.', + ], + ], 422); + } + $redisConf = base64_decode($request->redis_conf); + if (mb_detect_encoding($redisConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'redis_conf' => 'The redis_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('redis_conf', $redisConf); + } + $database = create_standalone_redis($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartDatabase::dispatch($database)->onQueue('high'); + } + + $database->refresh(); + $payload = [ + 'uuid' => $database->uuid, + 'internal_db_url' => $database->internal_db_url, + ]; + if ($database->is_public && $database->public_port) { + $payload['external_db_url'] = $database->external_db_url; + } + + return response()->json(serializeApiResponse($payload))->setStatusCode(201); + } elseif ($type === NewDatabaseTypes::DRAGONFLY) { + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password']; + $validator = customApiValidator($request->all(), [ + 'dragonfly_password' => 'string', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartDatabase::dispatch($database)->onQueue('high'); + } + + return response()->json(serializeApiResponse([ + 'uuid' => $database->uuid, + ]))->setStatusCode(201); + } elseif ($type === NewDatabaseTypes::KEYDB) { + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf']; + $validator = customApiValidator($request->all(), [ + 'keydb_password' => 'string', + 'keydb_conf' => 'string', + ]); + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + removeUnnecessaryFieldsFromRequest($request); + if ($request->has('keydb_conf')) { + if (! isBase64Encoded($request->keydb_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'keydb_conf' => 'The keydb_conf should be base64 encoded.', + ], + ], 422); + } + $keydbConf = base64_decode($request->keydb_conf); + if (mb_detect_encoding($keydbConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'keydb_conf' => 'The keydb_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('keydb_conf', $keydbConf); + } + $database = create_standalone_keydb($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartDatabase::dispatch($database)->onQueue('high'); + } + + $database->refresh(); + $payload = [ + 'uuid' => $database->uuid, + 'internal_db_url' => $database->internal_db_url, + ]; + if ($database->is_public && $database->public_port) { + $payload['external_db_url'] = $database->external_db_url; + } + + return response()->json(serializeApiResponse($payload))->setStatusCode(201); + } elseif ($type === NewDatabaseTypes::CLICKHOUSE) { + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password']; + $validator = customApiValidator($request->all(), [ + 'clickhouse_admin_user' => 'string', + 'clickhouse_admin_password' => 'string', + ]); + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartDatabase::dispatch($database)->onQueue('high'); + } + + $database->refresh(); + $payload = [ + 'uuid' => $database->uuid, + 'internal_db_url' => $database->internal_db_url, + ]; + if ($database->is_public && $database->public_port) { + $payload['external_db_url'] = $database->external_db_url; + } + + return response()->json(serializeApiResponse($payload))->setStatusCode(201); + } elseif ($type === NewDatabaseTypes::MONGODB) { + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database']; + $validator = customApiValidator($request->all(), [ + 'mongo_conf' => 'string', + 'mongo_initdb_root_username' => 'string', + 'mongo_initdb_root_password' => 'string', + 'mongo_initdb_init_database' => 'string', + ]); + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + removeUnnecessaryFieldsFromRequest($request); + if ($request->has('mongo_conf')) { + if (! isBase64Encoded($request->mongo_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mongo_conf' => 'The mongo_conf should be base64 encoded.', + ], + ], 422); + } + $mongoConf = base64_decode($request->mongo_conf); + if (mb_detect_encoding($mongoConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mongo_conf' => 'The mongo_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('mongo_conf', $mongoConf); + } + $database = create_standalone_mongodb($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartDatabase::dispatch($database)->onQueue('high'); + } + + $database->refresh(); + $payload = [ + 'uuid' => $database->uuid, + 'internal_db_url' => $database->internal_db_url, + ]; + if ($database->is_public && $database->public_port) { + $payload['external_db_url'] = $database->external_db_url; + } + + return response()->json(serializeApiResponse($payload))->setStatusCode(201); + } + + return response()->json(['message' => 'Invalid database type requested.'], 400); + } + + #[OA\Delete( + summary: 'Delete', + description: 'Delete database by UUID.', + path: '/databases/{uuid}', + operationId: 'delete-database-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the database.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Database deleted.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Database deleted.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function delete_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + $cleanup = filter_var($request->query->get('cleanup', true), FILTER_VALIDATE_BOOLEAN); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['message' => 'UUID is required.'], 404); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + + DeleteResourceJob::dispatch( + resource: $database, + deleteConfigurations: $request->query->get('delete_configurations', true), + deleteVolumes: $request->query->get('delete_volumes', true), + dockerCleanup: $request->query->get('docker_cleanup', true), + deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) + )->onQueue('high'); + + return response()->json([ + 'message' => 'Database deletion request queued.', + ]); + } + + #[OA\Get( + summary: 'Start', + description: 'Start database. `Post` request is also accepted.', + path: '/databases/{uuid}/start', + operationId: 'start-database-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the database.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Start database.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Database starting request queued.'], + ]) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function action_deploy(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + if (str($database->status)->contains('running')) { + return response()->json(['message' => 'Database is already running.'], 400); + } + StartDatabase::dispatch($database)->onQueue('high'); + + return response()->json( + [ + 'message' => 'Database starting request queued.', + ], + 200 + ); + } + + #[OA\Get( + summary: 'Stop', + description: 'Stop database. `Post` request is also accepted.', + path: '/databases/{uuid}/stop', + operationId: 'stop-database-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the database.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Stop database.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Database stopping request queued.'], + ]) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function action_stop(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + if (str($database->status)->contains('stopped') || str($database->status)->contains('exited')) { + return response()->json(['message' => 'Database is already stopped.'], 400); + } + StopDatabase::dispatch($database)->onQueue('high'); + + return response()->json( + [ + 'message' => 'Database stopping request queued.', + ], + 200 + ); + } + + #[OA\Get( + summary: 'Restart', + description: 'Restart database. `Post` request is also accepted.', + path: '/databases/{uuid}/restart', + operationId: 'restart-database-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the database.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Restart database.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Database restaring request queued.'], + ]) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function action_restart(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + RestartDatabase::dispatch($database)->onQueue('high'); + + return response()->json( + [ + 'message' => 'Database restarting request queued.', + ], + 200 + ); + } +} diff --git a/app/Http/Controllers/Api/Deploy.php b/app/Http/Controllers/Api/Deploy.php deleted file mode 100644 index d2abe2e31..000000000 --- a/app/Http/Controllers/Api/Deploy.php +++ /dev/null @@ -1,216 +0,0 @@ -get(); - $deployments_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $servers->pluck('id'))->get([ - 'id', - 'application_id', - 'application_name', - 'deployment_url', - 'pull_request_id', - 'server_name', - 'server_id', - 'status', - ])->sortBy('id')->toArray(); - - return response()->json($deployments_per_server, 200); - } - - public function deploy(Request $request) - { - $teamId = get_team_id_from_token(); - $uuids = $request->query->get('uuid'); - $tags = $request->query->get('tag'); - $force = $request->query->get('force') ?? false; - - if ($uuids && $tags) { - return response()->json(['error' => 'You can only use uuid or tag, not both.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); - } - if (is_null($teamId)) { - return invalid_token(); - } - if ($tags) { - return $this->by_tags($tags, $teamId, $force); - } elseif ($uuids) { - return $this->by_uuids($uuids, $teamId, $force); - } - - return response()->json(['error' => 'You must provide uuid or tag.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); - } - - private function by_uuids(string $uuid, int $teamId, bool $force = false) - { - $uuids = explode(',', $uuid); - $uuids = collect(array_filter($uuids)); - - if (count($uuids) === 0) { - return response()->json(['error' => 'No UUIDs provided.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); - } - $deployments = collect(); - $payload = collect(); - foreach ($uuids as $uuid) { - $resource = getResourceByUuid($uuid, $teamId); - if ($resource) { - ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force); - if ($deployment_uuid) { - $deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]); - } else { - $deployments->push(['message' => $return_message, 'resource_uuid' => $uuid]); - } - } - } - if ($deployments->count() > 0) { - $payload->put('deployments', $deployments->toArray()); - - return response()->json($payload->toArray(), 200); - } - - return response()->json(['error' => 'No resources found.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404); - } - - public function by_tags(string $tags, int $team_id, bool $force = false) - { - $tags = explode(',', $tags); - $tags = collect(array_filter($tags)); - - if (count($tags) === 0) { - return response()->json(['error' => 'No TAGs provided.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); - } - $message = collect([]); - $deployments = collect(); - $payload = collect(); - foreach ($tags as $tag) { - $found_tag = Tag::where(['name' => $tag, 'team_id' => $team_id])->first(); - if (! $found_tag) { - // $message->push("Tag {$tag} not found."); - continue; - } - $applications = $found_tag->applications()->get(); - $services = $found_tag->services()->get(); - if ($applications->count() === 0 && $services->count() === 0) { - $message->push("No resources found for tag {$tag}."); - - continue; - } - foreach ($applications as $resource) { - ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force); - if ($deployment_uuid) { - $deployments->push(['resource_uuid' => $resource->uuid, 'deployment_uuid' => $deployment_uuid->toString()]); - } - $message = $message->merge($return_message); - } - foreach ($services as $resource) { - ['message' => $return_message] = $this->deploy_resource($resource, $force); - $message = $message->merge($return_message); - } - } - ray($message); - if ($message->count() > 0) { - $payload->put('message', $message->toArray()); - if ($deployments->count() > 0) { - $payload->put('details', $deployments->toArray()); - } - - return response()->json($payload->toArray(), 200); - } - - return response()->json(['error' => 'No resources found with this tag.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404); - } - - public function deploy_resource($resource, bool $force = false): array - { - $message = null; - $deployment_uuid = null; - if (gettype($resource) !== 'object') { - return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid]; - } - $type = $resource?->getMorphClass(); - if ($type === 'App\Models\Application') { - $deployment_uuid = new Cuid2(7); - queue_application_deployment( - application: $resource, - deployment_uuid: $deployment_uuid, - force_rebuild: $force, - ); - $message = "Application {$resource->name} deployment queued."; - } elseif ($type === 'App\Models\StandalonePostgresql') { - StartPostgresql::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\StandaloneRedis') { - StartRedis::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\StandaloneKeydb') { - StartKeydb::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\StandaloneDragonfly') { - StartDragonfly::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\StandaloneClickhouse') { - StartClickhouse::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\StandaloneMongodb') { - StartMongodb::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\StandaloneMysql') { - StartMysql::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\StandaloneMariadb') { - StartMariadb::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\Service') { - StartService::run($resource); - $message = "Service {$resource->name} started. It could take a while, be patient."; - } - - return ['message' => $message, 'deployment_uuid' => $deployment_uuid]; - } -} diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php new file mode 100644 index 000000000..59b199d87 --- /dev/null +++ b/app/Http/Controllers/Api/DeployController.php @@ -0,0 +1,320 @@ +user()->currentAccessToken(); + if ($token->can('view:sensitive')) { + return serializeApiResponse($deployment); + } + + $deployment->makeHidden([ + 'logs', + ]); + + return serializeApiResponse($deployment); + } + + #[OA\Get( + summary: 'List', + description: 'List currently running deployments', + path: '/deployments', + operationId: 'list-deployments', + security: [ + ['bearerAuth' => []], + ], + tags: ['Deployments'], + responses: [ + new OA\Response( + response: 200, + description: 'Get all currently running deployments.', + content: [ + + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/ApplicationDeploymentQueue'), + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function deployments(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $servers = Server::whereTeamId($teamId)->get(); + $deployments_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $servers->pluck('id'))->get()->sortBy('id'); + $deployments_per_server = $deployments_per_server->map(function ($deployment) { + return $this->removeSensitiveData($deployment); + }); + + return response()->json($deployments_per_server); + } + + #[OA\Get( + summary: 'Get', + description: 'Get deployment by UUID.', + path: '/deployments/{uuid}', + operationId: 'get-deployment-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Deployments'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Deployment UUID', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Get deployment by UUID.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + ref: '#/components/schemas/ApplicationDeploymentQueue', + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function deployment_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $deployment = ApplicationDeploymentQueue::where('deployment_uuid', $uuid)->first(); + if (! $deployment) { + return response()->json(['message' => 'Deployment not found.'], 404); + } + + return response()->json($this->removeSensitiveData($deployment)); + } + + #[OA\Get( + summary: 'Deploy', + description: 'Deploy by tag or uuid. `Post` request also accepted.', + path: '/deploy', + operationId: 'deploy-by-tag-or-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Deployments'], + parameters: [ + new OA\Parameter(name: 'tag', in: 'query', description: 'Tag name(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'uuid', in: 'query', description: 'Resource UUID(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'force', in: 'query', description: 'Force rebuild (without cache)', schema: new OA\Schema(type: 'boolean')), + ], + + responses: [ + new OA\Response( + response: 200, + description: 'Get deployment(s) UUID\'s', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'deployments' => new OA\Property( + property: 'deployments', + type: 'array', + items: new OA\Items( + type: 'object', + properties: [ + 'message' => ['type' => 'string'], + 'resource_uuid' => ['type' => 'string'], + 'deployment_uuid' => ['type' => 'string'], + ] + ), + ), + ], + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + + ] + )] + public function deploy(Request $request) + { + $teamId = getTeamIdFromToken(); + $uuids = $request->query->get('uuid'); + $tags = $request->query->get('tag'); + $force = $request->query->get('force') ?? false; + + if ($uuids && $tags) { + return response()->json(['message' => 'You can only use uuid or tag, not both.'], 400); + } + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if ($tags) { + return $this->by_tags($tags, $teamId, $force); + } elseif ($uuids) { + return $this->by_uuids($uuids, $teamId, $force); + } + + return response()->json(['message' => 'You must provide uuid or tag.'], 400); + } + + private function by_uuids(string $uuid, int $teamId, bool $force = false) + { + $uuids = explode(',', $uuid); + $uuids = collect(array_filter($uuids)); + + if (count($uuids) === 0) { + return response()->json(['message' => 'No UUIDs provided.'], 400); + } + $deployments = collect(); + $payload = collect(); + foreach ($uuids as $uuid) { + $resource = getResourceByUuid($uuid, $teamId); + if ($resource) { + ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force); + if ($deployment_uuid) { + $deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]); + } else { + $deployments->push(['message' => $return_message, 'resource_uuid' => $uuid]); + } + } + } + if ($deployments->count() > 0) { + $payload->put('deployments', $deployments->toArray()); + + return response()->json(serializeApiResponse($payload->toArray())); + } + + return response()->json(['message' => 'No resources found.'], 404); + } + + public function by_tags(string $tags, int $team_id, bool $force = false) + { + $tags = explode(',', $tags); + $tags = collect(array_filter($tags)); + + if (count($tags) === 0) { + return response()->json(['message' => 'No TAGs provided.'], 400); + } + $message = collect([]); + $deployments = collect(); + $payload = collect(); + foreach ($tags as $tag) { + $found_tag = Tag::where(['name' => $tag, 'team_id' => $team_id])->first(); + if (! $found_tag) { + // $message->push("Tag {$tag} not found."); + continue; + } + $applications = $found_tag->applications()->get(); + $services = $found_tag->services()->get(); + if ($applications->count() === 0 && $services->count() === 0) { + $message->push("No resources found for tag {$tag}."); + + continue; + } + foreach ($applications as $resource) { + ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force); + if ($deployment_uuid) { + $deployments->push(['resource_uuid' => $resource->uuid, 'deployment_uuid' => $deployment_uuid->toString()]); + } + $message = $message->merge($return_message); + } + foreach ($services as $resource) { + ['message' => $return_message] = $this->deploy_resource($resource, $force); + $message = $message->merge($return_message); + } + } + if ($message->count() > 0) { + $payload->put('message', $message->toArray()); + if ($deployments->count() > 0) { + $payload->put('details', $deployments->toArray()); + } + + return response()->json(serializeApiResponse($payload->toArray())); + } + + return response()->json(['message' => 'No resources found with this tag.'], 404); + } + + public function deploy_resource($resource, bool $force = false): array + { + $message = null; + $deployment_uuid = null; + if (gettype($resource) !== 'object') { + return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid]; + } + switch ($resource?->getMorphClass()) { + case \App\Models\Application::class: + $deployment_uuid = new Cuid2; + queue_application_deployment( + application: $resource, + deployment_uuid: $deployment_uuid, + force_rebuild: $force, + ); + $message = "Application {$resource->name} deployment queued."; + break; + case \App\Models\Service::class: + StartService::run($resource); + $message = "Service {$resource->name} started. It could take a while, be patient."; + break; + default: + // Database resource + StartDatabase::dispatch($resource)->onQueue('high'); + $resource->update([ + 'started_at' => now(), + ]); + $message = "Database {$resource->name} started."; + break; + } + + return ['message' => $message, 'deployment_uuid' => $deployment_uuid]; + } +} diff --git a/app/Http/Controllers/Api/Domains.php b/app/Http/Controllers/Api/Domains.php deleted file mode 100644 index c27ddf620..000000000 --- a/app/Http/Controllers/Api/Domains.php +++ /dev/null @@ -1,104 +0,0 @@ -get(); - $domains = collect(); - $applications = $projects->pluck('applications')->flatten(); - $settings = InstanceSettings::get(); - if ($applications->count() > 0) { - foreach ($applications as $application) { - $ip = $application->destination->server->ip; - $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) { - return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', ''); - }); - if ($ip === 'host.docker.internal') { - if ($settings->public_ipv4) { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $settings->public_ipv4, - ]); - } - if ($settings->public_ipv6) { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $settings->public_ipv6, - ]); - } - if (! $settings->public_ipv4 && ! $settings->public_ipv6) { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $ip, - ]); - } - } else { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $ip, - ]); - } - } - } - $services = $projects->pluck('services')->flatten(); - if ($services->count() > 0) { - foreach ($services as $service) { - $service_applications = $service->applications; - if ($service_applications->count() > 0) { - foreach ($service_applications as $application) { - $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) { - return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', ''); - }); - if ($ip === 'host.docker.internal') { - if ($settings->public_ipv4) { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $settings->public_ipv4, - ]); - } - if ($settings->public_ipv6) { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $settings->public_ipv6, - ]); - } - if (! $settings->public_ipv4 && ! $settings->public_ipv6) { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $ip, - ]); - } - } else { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $ip, - ]); - } - } - } - } - } - $domains = $domains->groupBy('ip')->map(function ($domain) { - return $domain->pluck('domain')->flatten(); - })->map(function ($domain, $ip) { - return [ - 'ip' => $ip, - 'domains' => $domain, - ]; - })->values(); - - return response()->json($domains); - } -} diff --git a/app/Http/Controllers/Api/OpenApi.php b/app/Http/Controllers/Api/OpenApi.php new file mode 100644 index 000000000..60337a76c --- /dev/null +++ b/app/Http/Controllers/Api/OpenApi.php @@ -0,0 +1,51 @@ + []], + ], + responses: [ + new OA\Response( + response: 200, + description: 'Returns the version of the application', + content: new OA\JsonContent( + type: 'string', + example: 'v4.0.0', + )), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function version(Request $request) + { + return response(config('version')); + } + + #[OA\Get( + summary: 'Enable API', + description: 'Enable API (only with root permissions).', + path: '/enable', + operationId: 'enable-api', + security: [ + ['bearerAuth' => []], + ], + responses: [ + new OA\Response( + response: 200, + description: 'Enable API.', + content: new OA\JsonContent( + type: 'object', + properties: [ + new OA\Property(property: 'message', type: 'string', example: 'API enabled.'), + ] + )), + new OA\Response( + response: 403, + description: 'You are not allowed to enable the API.', + content: new OA\JsonContent( + type: 'object', + properties: [ + new OA\Property(property: 'message', type: 'string', example: 'You are not allowed to enable the API.'), + ] + )), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function enable_api(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if ($teamId !== '0') { + return response()->json(['message' => 'You are not allowed to enable the API.'], 403); + } + $settings = instanceSettings(); + $settings->update(['is_api_enabled' => true]); + + return response()->json(['message' => 'API enabled.'], 200); + } + + #[OA\Get( + summary: 'Disable API', + description: 'Disable API (only with root permissions).', + path: '/disable', + operationId: 'disable-api', + security: [ + ['bearerAuth' => []], + ], + responses: [ + new OA\Response( + response: 200, + description: 'Disable API.', + content: new OA\JsonContent( + type: 'object', + properties: [ + new OA\Property(property: 'message', type: 'string', example: 'API disabled.'), + ] + )), + new OA\Response( + response: 403, + description: 'You are not allowed to disable the API.', + content: new OA\JsonContent( + type: 'object', + properties: [ + new OA\Property(property: 'message', type: 'string', example: 'You are not allowed to disable the API.'), + ] + )), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function disable_api(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if ($teamId !== '0') { + return response()->json(['message' => 'You are not allowed to disable the API.'], 403); + } + $settings = instanceSettings(); + $settings->update(['is_api_enabled' => false]); + + return response()->json(['message' => 'API disabled.'], 200); + } + + public function feedback(Request $request) + { + $content = $request->input('content'); + $webhook_url = config('coolify.feedback_discord_webhook'); + if ($webhook_url) { + Http::post($webhook_url, [ + 'content' => $content, + ]); + } + + return response()->json(['message' => 'Feedback sent.'], 200); + } + + #[OA\Get( + summary: 'Healthcheck', + description: 'Healthcheck endpoint.', + path: '/health', + operationId: 'healthcheck', + responses: [ + new OA\Response( + response: 200, + description: 'Healthcheck endpoint.', + content: new OA\JsonContent( + type: 'string', + example: 'OK', + )), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function healthcheck(Request $request) + { + return 'OK'; + } +} diff --git a/app/Http/Controllers/Api/Project.php b/app/Http/Controllers/Api/Project.php deleted file mode 100644 index baaf1eacb..000000000 --- a/app/Http/Controllers/Api/Project.php +++ /dev/null @@ -1,44 +0,0 @@ -select('id', 'name', 'uuid')->get(); - - return response()->json($projects); - } - - public function project_by_uuid(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']); - - return response()->json($project); - } - - public function environment_details(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); - $environment = $project->environments()->whereName(request()->environment_name)->first()->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']); - - return response()->json($environment); - } -} diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php new file mode 100644 index 000000000..1d89c82ed --- /dev/null +++ b/app/Http/Controllers/Api/ProjectController.php @@ -0,0 +1,433 @@ + []], + ], + tags: ['Projects'], + responses: [ + new OA\Response( + response: 200, + description: 'Get all projects.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/Project') + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function projects(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $projects = Project::whereTeamId($teamId)->select('id', 'name', 'description', 'uuid')->get(); + + return response()->json(serializeApiResponse($projects), + ); + } + + #[OA\Get( + summary: 'Get', + description: 'Get project by UUID.', + path: '/projects/{uuid}', + operationId: 'get-project-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Projects'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Project details', + content: new OA\JsonContent(ref: '#/components/schemas/Project')), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + description: 'Project not found.', + ), + ] + )] + public function project_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $project = Project::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']); + if (! $project) { + return response()->json(['message' => 'Project not found.'], 404); + } + + return response()->json( + serializeApiResponse($project), + ); + } + + #[OA\Get( + summary: 'Environment', + description: 'Get environment by name.', + path: '/projects/{uuid}/{environment_name}', + operationId: 'get-environment-by-name', + security: [ + ['bearerAuth' => []], + ], + tags: ['Projects'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'environment_name', in: 'path', required: true, description: 'Environment name', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Environment details', + content: new OA\JsonContent(ref: '#/components/schemas/Environment')), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function environment_details(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['message' => 'UUID is required.'], 422); + } + if (! $request->environment_name) { + return response()->json(['message' => 'Environment name is required.'], 422); + } + $project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first(); + if (! $project) { + return response()->json(['message' => 'Project not found.'], 404); + } + $environment = $project->environments()->whereName($request->environment_name)->first(); + if (! $environment) { + return response()->json(['message' => 'Environment not found.'], 404); + } + $environment = $environment->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']); + + return response()->json(serializeApiResponse($environment)); + } + + #[OA\Post( + summary: 'Create', + description: 'Create Project.', + path: '/projects', + operationId: 'create-project', + security: [ + ['bearerAuth' => []], + ], + tags: ['Projects'], + requestBody: new OA\RequestBody( + required: true, + description: 'Project created.', + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'name' => ['type' => 'string', 'description' => 'The name of the project.'], + 'description' => ['type' => 'string', 'description' => 'The description of the project.'], + ], + ), + ), + ), + responses: [ + new OA\Response( + response: 201, + description: 'Project created.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'uuid' => ['type' => 'string', 'example' => 'og888os', 'description' => 'The UUID of the project.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function create_project(Request $request) + { + $allowedFields = ['name', 'description']; + + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255|required', + 'description' => 'string|nullable', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + + $project = Project::create([ + 'name' => $request->name, + 'description' => $request->description, + 'team_id' => $teamId, + ]); + + return response()->json([ + 'uuid' => $project->uuid, + ])->setStatusCode(201); + } + + #[OA\Patch( + summary: 'Update', + description: 'Update Project.', + path: '/projects/{uuid}', + operationId: 'update-project-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Projects'], + requestBody: new OA\RequestBody( + required: true, + description: 'Project updated.', + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'name' => ['type' => 'string', 'description' => 'The name of the project.'], + 'description' => ['type' => 'string', 'description' => 'The description of the project.'], + ], + ), + ), + ), + responses: [ + new OA\Response( + response: 201, + description: 'Project updated.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'uuid' => ['type' => 'string', 'example' => 'og888os'], + 'name' => ['type' => 'string', 'example' => 'Project Name'], + 'description' => ['type' => 'string', 'example' => 'Project Description'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function update_project(Request $request) + { + $allowedFields = ['name', 'description']; + + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255|nullable', + 'description' => 'string|nullable', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $uuid = $request->uuid; + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 422); + } + + $project = Project::whereTeamId($teamId)->whereUuid($uuid)->first(); + if (! $project) { + return response()->json(['message' => 'Project not found.'], 404); + } + + $project->update($request->only($allowedFields)); + + return response()->json([ + 'uuid' => $project->uuid, + 'name' => $project->name, + 'description' => $project->description, + ])->setStatusCode(201); + } + + #[OA\Delete( + summary: 'Delete', + description: 'Delete project by UUID.', + path: '/projects/{uuid}', + operationId: 'delete-project-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Projects'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Project deleted.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Project deleted.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function delete_project(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + if (! $request->uuid) { + return response()->json(['message' => 'UUID is required.'], 422); + } + $project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first(); + if (! $project) { + return response()->json(['message' => 'Project not found.'], 404); + } + if (! $project->isEmpty()) { + return response()->json(['message' => 'Project has resources, so it cannot be deleted.'], 400); + } + + $project->delete(); + + return response()->json(['message' => 'Project deleted.']); + } +} diff --git a/app/Http/Controllers/Api/Resources.php b/app/Http/Controllers/Api/Resources.php deleted file mode 100644 index 0d538b62e..000000000 --- a/app/Http/Controllers/Api/Resources.php +++ /dev/null @@ -1,39 +0,0 @@ -get(); - $resources = collect(); - $resources->push($projects->pluck('applications')->flatten()); - $resources->push($projects->pluck('services')->flatten()); - foreach (collect(DATABASE_TYPES) as $db) { - $resources->push($projects->pluck(str($db)->plural(2))->flatten()); - } - $resources = $resources->flatten(); - $resources = $resources->map(function ($resource) { - $payload = $resource->toArray(); - if ($resource->getMorphClass() === 'App\Models\Service') { - $payload['status'] = $resource->status(); - } else { - $payload['status'] = $resource->status; - } - $payload['type'] = $resource->type(); - - return $payload; - }); - - return response()->json($resources); - } -} diff --git a/app/Http/Controllers/Api/ResourcesController.php b/app/Http/Controllers/Api/ResourcesController.php new file mode 100644 index 000000000..4180cef9a --- /dev/null +++ b/app/Http/Controllers/Api/ResourcesController.php @@ -0,0 +1,68 @@ + []], + ], + tags: ['Resources'], + responses: [ + new OA\Response( + response: 200, + description: 'Get all resources', + content: new OA\JsonContent( + type: 'string', + example: 'Content is very complex. Will be implemented later.', + ), + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function resources(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $projects = Project::where('team_id', $teamId)->get(); + $resources = collect(); + $resources->push($projects->pluck('applications')->flatten()); + $resources->push($projects->pluck('services')->flatten()); + foreach (collect(DATABASE_TYPES) as $db) { + $resources->push($projects->pluck(str($db)->plural(2))->flatten()); + } + $resources = $resources->flatten(); + $resources = $resources->map(function ($resource) { + $payload = $resource->toArray(); + if ($resource->getMorphClass() === \App\Models\Service::class) { + $payload['status'] = $resource->status(); + } else { + $payload['status'] = $resource->status; + } + $payload['type'] = $resource->type(); + + return $payload; + }); + + return response()->json(serializeApiResponse($resources)); + } +} diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php new file mode 100644 index 000000000..b7190ab1e --- /dev/null +++ b/app/Http/Controllers/Api/SecurityController.php @@ -0,0 +1,370 @@ +user()->currentAccessToken(); + if ($token->can('view:sensitive')) { + return serializeApiResponse($team); + } + $team->makeHidden([ + 'private_key', + ]); + + return serializeApiResponse($team); + } + + #[OA\Get( + summary: 'List', + description: 'List all private keys.', + path: '/security/keys', + operationId: 'list-private-keys', + security: [ + ['bearerAuth' => []], + ], + tags: ['Private Keys'], + responses: [ + new OA\Response( + response: 200, + description: 'Get all private keys.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/PrivateKey') + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function keys(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $keys = PrivateKey::where('team_id', $teamId)->get(); + + return response()->json($this->removeSensitiveData($keys)); + } + + #[OA\Get( + summary: 'Get', + description: 'Get key by UUID.', + path: '/security/keys/{uuid}', + operationId: 'get-private-key-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Private Keys'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key UUID', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Get all private keys.', + content: new OA\JsonContent(ref: '#/components/schemas/PrivateKey') + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + description: 'Private Key not found.', + ), + ] + )] + public function key_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $key = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first(); + + if (is_null($key)) { + return response()->json([ + 'message' => 'Private Key not found.', + ], 404); + } + + return response()->json($this->removeSensitiveData($key)); + } + + #[OA\Post( + summary: 'Create', + description: 'Create a new private key.', + path: '/security/keys', + operationId: 'create-private-key', + security: [ + ['bearerAuth' => []], + ], + tags: ['Private Keys'], + requestBody: new OA\RequestBody( + required: true, + content: [ + 'application/json' => new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['private_key'], + properties: [ + 'name' => ['type' => 'string'], + 'description' => ['type' => 'string'], + 'private_key' => ['type' => 'string'], + ], + additionalProperties: false, + ) + ), + ] + ), + responses: [ + new OA\Response( + response: 201, + description: 'The created private key\'s UUID.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'uuid' => ['type' => 'string'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_key(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255', + 'description' => 'string|max:255', + 'private_key' => 'required|string', + ]); + + if ($validator->fails()) { + $errors = $validator->errors(); + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + if (! $request->name) { + $request->offsetSet('name', generate_random_name()); + } + if (! $request->description) { + $request->offsetSet('description', 'Created by Coolify via API'); + } + $key = PrivateKey::create([ + 'team_id' => $teamId, + 'name' => $request->name, + 'description' => $request->description, + 'private_key' => $request->private_key, + ]); + + return response()->json(serializeApiResponse([ + 'uuid' => $key->uuid, + ]))->setStatusCode(201); + } + + #[OA\Patch( + summary: 'Update', + description: 'Update a private key.', + path: '/security/keys', + operationId: 'update-private-key', + security: [ + ['bearerAuth' => []], + ], + tags: ['Private Keys'], + requestBody: new OA\RequestBody( + required: true, + content: [ + 'application/json' => new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['private_key'], + properties: [ + 'name' => ['type' => 'string'], + 'description' => ['type' => 'string'], + 'private_key' => ['type' => 'string'], + ], + additionalProperties: false, + ) + ), + ] + ), + responses: [ + new OA\Response( + response: 201, + description: 'The updated private key\'s UUID.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'uuid' => ['type' => 'string'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function update_key(Request $request) + { + $allowedFields = ['name', 'description', 'private_key']; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255', + 'description' => 'string|max:255', + 'private_key' => 'required|string', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $foundKey = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first(); + if (is_null($foundKey)) { + return response()->json([ + 'message' => 'Private Key not found.', + ], 404); + } + $foundKey->update($request->all()); + + return response()->json(serializeApiResponse([ + 'uuid' => $foundKey->uuid, + ]))->setStatusCode(201); + } + + #[OA\Delete( + summary: 'Delete', + description: 'Delete a private key.', + path: '/security/keys/{uuid}', + operationId: 'delete-private-key-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Private Keys'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key UUID', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Private Key deleted.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Private Key deleted.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + description: 'Private Key not found.', + ), + ] + )] + public function delete_key(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['message' => 'UUID is required.'], 422); + } + + $key = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first(); + if (is_null($key)) { + return response()->json(['message' => 'Private Key not found.'], 404); + } + $key->forceDelete(); + + return response()->json([ + 'message' => 'Private Key deleted.', + ]); + } +} diff --git a/app/Http/Controllers/Api/Server.php b/app/Http/Controllers/Api/Server.php deleted file mode 100644 index 9f88a3b28..000000000 --- a/app/Http/Controllers/Api/Server.php +++ /dev/null @@ -1,62 +0,0 @@ -select('id', 'name', 'uuid', 'ip', 'user', 'port')->get()->load(['settings'])->map(function ($server) { - $server['is_reachable'] = $server->settings->is_reachable; - $server['is_usable'] = $server->settings->is_usable; - - return $server; - }); - - return response()->json($servers); - } - - public function server_by_uuid(Request $request) - { - $with_resources = $request->query('resources'); - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); - if (is_null($server)) { - return response()->json(['error' => 'Server not found.'], 404); - } - if ($with_resources) { - $server['resources'] = $server->definedResources()->map(function ($resource) { - $payload = [ - 'id' => $resource->id, - 'uuid' => $resource->uuid, - 'name' => $resource->name, - 'type' => $resource->type(), - 'created_at' => $resource->created_at, - 'updated_at' => $resource->updated_at, - ]; - if ($resource->type() === 'service') { - $payload['status'] = $resource->status(); - } else { - $payload['status'] = $resource->status; - } - - return $payload; - }); - } else { - $server->load(['settings']); - } - - return response()->json($server); - } -} diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php new file mode 100644 index 000000000..024ef35fa --- /dev/null +++ b/app/Http/Controllers/Api/ServersController.php @@ -0,0 +1,794 @@ +user()->currentAccessToken(); + if ($token->can('view:sensitive')) { + return serializeApiResponse($settings); + } + $settings = $settings->makeHidden([ + 'sentinel_token', + ]); + + return serializeApiResponse($settings); + } + + private function removeSensitiveData($server) + { + $token = auth()->user()->currentAccessToken(); + $server->makeHidden([ + 'id', + ]); + if ($token->can('view:sensitive')) { + return serializeApiResponse($server); + } + + return serializeApiResponse($server); + } + + #[OA\Get( + summary: 'List', + description: 'List all servers.', + path: '/servers', + operationId: 'list-servers', + security: [ + ['bearerAuth' => []], + ], + tags: ['Servers'], + responses: [ + new OA\Response( + response: 200, + description: 'Get all servers.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/Server') + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function servers(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $servers = ModelsServer::whereTeamId($teamId)->select('id', 'name', 'uuid', 'ip', 'user', 'port', 'description')->get()->load(['settings'])->map(function ($server) { + $server['is_reachable'] = $server->settings->is_reachable; + $server['is_usable'] = $server->settings->is_usable; + + return $server; + }); + $servers = $servers->map(function ($server) { + $settings = $this->removeSensitiveDataFromSettings($server->settings); + $server = $this->removeSensitiveData($server); + data_set($server, 'settings', $settings); + + return $server; + }); + + return response()->json($servers); + } + + #[OA\Get( + summary: 'Get', + description: 'Get server by UUID.', + path: '/servers/{uuid}', + operationId: 'get-server-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Servers'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s UUID', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Get server by UUID', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + ref: '#/components/schemas/Server' + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function server_by_uuid(Request $request) + { + $with_resources = $request->query('resources'); + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); + if (is_null($server)) { + return response()->json(['message' => 'Server not found.'], 404); + } + if ($with_resources) { + $server['resources'] = $server->definedResources()->map(function ($resource) { + $payload = [ + 'id' => $resource->id, + 'uuid' => $resource->uuid, + 'name' => $resource->name, + 'type' => $resource->type(), + 'created_at' => $resource->created_at, + 'updated_at' => $resource->updated_at, + ]; + if ($resource->type() === 'service') { + $payload['status'] = $resource->status(); + } else { + $payload['status'] = $resource->status; + } + + return $payload; + }); + } else { + $server->load(['settings']); + } + + $settings = $this->removeSensitiveDataFromSettings($server->settings); + $server = $this->removeSensitiveData($server); + data_set($server, 'settings', $settings); + + return response()->json(serializeApiResponse($server)); + } + + #[OA\Get( + summary: 'Resources', + description: 'Get resources by server.', + path: '/servers/{uuid}/resources', + operationId: 'get-resources-by-server-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Servers'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s UUID', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Get resources by server', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items( + type: 'object', + properties: [ + 'id' => ['type' => 'integer'], + 'uuid' => ['type' => 'string'], + 'name' => ['type' => 'string'], + 'type' => ['type' => 'string'], + 'created_at' => ['type' => 'string'], + 'updated_at' => ['type' => 'string'], + 'status' => ['type' => 'string'], + ] + ) + )), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function resources_by_server(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); + if (is_null($server)) { + return response()->json(['message' => 'Server not found.'], 404); + } + $server['resources'] = $server->definedResources()->map(function ($resource) { + $payload = [ + 'id' => $resource->id, + 'uuid' => $resource->uuid, + 'name' => $resource->name, + 'type' => $resource->type(), + 'created_at' => $resource->created_at, + 'updated_at' => $resource->updated_at, + ]; + if ($resource->type() === 'service') { + $payload['status'] = $resource->status(); + } else { + $payload['status'] = $resource->status; + } + + return $payload; + }); + $server = $this->removeSensitiveData($server); + + return response()->json(serializeApiResponse(data_get($server, 'resources'))); + } + + #[OA\Get( + summary: 'Domains', + description: 'Get domains by server.', + path: '/servers/{uuid}/domains', + operationId: 'get-domains-by-server-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Servers'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s UUID', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Get domains by server', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items( + type: 'object', + properties: [ + 'ip' => ['type' => 'string'], + 'domains' => ['type' => 'array', 'items' => ['type' => 'string']], + ] + ) + )), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function domains_by_server(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->get('uuid'); + if ($uuid) { + $domains = Application::getDomainsByUuid($uuid); + + return response()->json(serializeApiResponse($domains)); + } + $projects = Project::where('team_id', $teamId)->get(); + $domains = collect(); + $applications = $projects->pluck('applications')->flatten(); + $settings = instanceSettings(); + if ($applications->count() > 0) { + foreach ($applications as $application) { + $ip = $application->destination->server->ip; + $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) { + $f = str($fqdn)->replace('http://', '')->replace('https://', '')->explode('/'); + + return str(str($f[0])->explode(':')[0]); + })->filter(function (Stringable $fqdn) { + return $fqdn->isNotEmpty(); + }); + + if ($ip === 'host.docker.internal') { + if ($settings->public_ipv4) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $settings->public_ipv4, + ]); + } + if ($settings->public_ipv6) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $settings->public_ipv6, + ]); + } + if (! $settings->public_ipv4 && ! $settings->public_ipv6) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $ip, + ]); + } + } else { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $ip, + ]); + } + } + } + $services = $projects->pluck('services')->flatten(); + if ($services->count() > 0) { + foreach ($services as $service) { + $service_applications = $service->applications; + if ($service_applications->count() > 0) { + foreach ($service_applications as $application) { + $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) { + $f = str($fqdn)->replace('http://', '')->replace('https://', '')->explode('/'); + + return str(str($f[0])->explode(':')[0]); + })->filter(function (Stringable $fqdn) { + return $fqdn->isNotEmpty(); + }); + if ($ip === 'host.docker.internal') { + if ($settings->public_ipv4) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $settings->public_ipv4, + ]); + } + if ($settings->public_ipv6) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $settings->public_ipv6, + ]); + } + if (! $settings->public_ipv4 && ! $settings->public_ipv6) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $ip, + ]); + } + } else { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $ip, + ]); + } + } + } + } + } + $domains = $domains->groupBy('ip')->map(function ($domain) { + return $domain->pluck('domain')->flatten(); + })->map(function ($domain, $ip) { + return [ + 'ip' => $ip, + 'domains' => $domain, + ]; + })->values(); + + return response()->json(serializeApiResponse($domains)); + } + + #[OA\Post( + summary: 'Create', + description: 'Create Server.', + path: '/servers', + operationId: 'create-server', + security: [ + ['bearerAuth' => []], + ], + tags: ['Servers'], + requestBody: new OA\RequestBody( + required: true, + description: 'Server created.', + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'name' => ['type' => 'string', 'example' => 'My Server', 'description' => 'The name of the server.'], + 'description' => ['type' => 'string', 'example' => 'My Server Description', 'description' => 'The description of the server.'], + 'ip' => ['type' => 'string', 'example' => '127.0.0.1', 'description' => 'The IP of the server.'], + 'port' => ['type' => 'integer', 'example' => 22, 'description' => 'The port of the server.'], + 'user' => ['type' => 'string', 'example' => 'root', 'description' => 'The user of the server.'], + 'private_key_uuid' => ['type' => 'string', 'example' => 'og888os', 'description' => 'The UUID of the private key.'], + 'is_build_server' => ['type' => 'boolean', 'example' => false, 'description' => 'Is build server.'], + 'instant_validate' => ['type' => 'boolean', 'example' => false, 'description' => 'Instant validate.'], + ], + ), + ), + ), + responses: [ + new OA\Response( + response: 201, + description: 'Server created.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'uuid' => ['type' => 'string', 'example' => 'og888os', 'description' => 'The UUID of the server.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function create_server(Request $request) + { + $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate']; + + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255', + 'description' => 'string|nullable', + 'ip' => 'string|required', + 'port' => 'integer|nullable', + 'private_key_uuid' => 'string|required', + 'user' => 'string|nullable', + 'is_build_server' => 'boolean|nullable', + 'instant_validate' => 'boolean|nullable', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + if (! $request->name) { + $request->offsetSet('name', generate_random_name()); + } + if (! $request->user) { + $request->offsetSet('user', 'root'); + } + if (is_null($request->port)) { + $request->offsetSet('port', 22); + } + if (is_null($request->is_build_server)) { + $request->offsetSet('is_build_server', false); + } + if (is_null($request->instant_validate)) { + $request->offsetSet('instant_validate', false); + } + $privateKey = PrivateKey::whereTeamId($teamId)->whereUuid($request->private_key_uuid)->first(); + if (! $privateKey) { + return response()->json(['message' => 'Private key not found.'], 404); + } + $allServers = ModelsServer::whereIp($request->ip)->get(); + if ($allServers->count() > 0) { + return response()->json(['message' => 'Server with this IP already exists.'], 400); + } + + $server = ModelsServer::create([ + 'name' => $request->name, + 'description' => $request->description, + 'ip' => $request->ip, + 'port' => $request->port, + 'user' => $request->user, + 'private_key_id' => $privateKey->id, + 'team_id' => $teamId, + 'proxy' => [ + 'type' => ProxyTypes::TRAEFIK->value, + 'status' => ProxyStatus::EXITED->value, + ], + ]); + $server->settings()->update([ + 'is_build_server' => $request->is_build_server, + ]); + if ($request->instant_validate) { + ValidateServer::dispatch($server)->onQueue('high'); + } + + return response()->json([ + 'uuid' => $server->uuid, + ])->setStatusCode(201); + } + + #[OA\Patch( + summary: 'Update', + description: 'Update Server.', + path: '/servers/{uuid}', + operationId: 'update-server-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Servers'], + requestBody: new OA\RequestBody( + required: true, + description: 'Server updated.', + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'name' => ['type' => 'string', 'description' => 'The name of the server.'], + 'description' => ['type' => 'string', 'description' => 'The description of the server.'], + 'ip' => ['type' => 'string', 'description' => 'The IP of the server.'], + 'port' => ['type' => 'integer', 'description' => 'The port of the server.'], + 'user' => ['type' => 'string', 'description' => 'The user of the server.'], + 'private_key_uuid' => ['type' => 'string', 'description' => 'The UUID of the private key.'], + 'is_build_server' => ['type' => 'boolean', 'description' => 'Is build server.'], + 'instant_validate' => ['type' => 'boolean', 'description' => 'Instant validate.'], + ], + ), + ), + ), + responses: [ + new OA\Response( + response: 201, + description: 'Server updated.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/Server') + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function update_server(Request $request) + { + $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate']; + + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255|nullable', + 'description' => 'string|nullable', + 'ip' => 'string|nullable', + 'port' => 'integer|nullable', + 'private_key_uuid' => 'string|nullable', + 'user' => 'string|nullable', + 'is_build_server' => 'boolean|nullable', + 'instant_validate' => 'boolean|nullable', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $server = ModelsServer::whereTeamId($teamId)->whereUuid($request->uuid)->first(); + if (! $server) { + return response()->json(['message' => 'Server not found.'], 404); + } + $server->update($request->only(['name', 'description', 'ip', 'port', 'user'])); + if ($request->is_build_server) { + $server->settings()->update([ + 'is_build_server' => $request->is_build_server, + ]); + } + if ($request->instant_validate) { + ValidateServer::dispatch($server)->onQueue('high'); + } + + return response()->json(serializeApiResponse($server))->setStatusCode(201); + } + + #[OA\Delete( + summary: 'Delete', + description: 'Delete server by UUID.', + path: '/servers/{uuid}', + operationId: 'delete-server-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Servers'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the server.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Server deleted.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Server deleted.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function delete_server(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + if (! $request->uuid) { + return response()->json(['message' => 'Uuid is required.'], 422); + } + $server = ModelsServer::whereTeamId($teamId)->whereUuid($request->uuid)->first(); + + if (! $server) { + return response()->json(['message' => 'Server not found.'], 404); + } + if ($server->definedResources()->count() > 0) { + return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400); + } + $server->delete(); + DeleteServer::dispatch($server); + + return response()->json(['message' => 'Server deleted.']); + } + + #[OA\Get( + summary: 'Validate', + description: 'Validate server by UUID.', + path: '/servers/{uuid}/validate', + operationId: 'validate-server-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Servers'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server UUID', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 201, + description: 'Server validation started.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Validation started.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function validate_server(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + if (! $request->uuid) { + return response()->json(['message' => 'Uuid is required.'], 422); + } + $server = ModelsServer::whereTeamId($teamId)->whereUuid($request->uuid)->first(); + + if (! $server) { + return response()->json(['message' => 'Server not found.'], 404); + } + ValidateServer::dispatch($server)->onQueue('high'); + + return response()->json(['message' => 'Validation started.']); + } +} diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php new file mode 100644 index 000000000..bdb5612ad --- /dev/null +++ b/app/Http/Controllers/Api/ServicesController.php @@ -0,0 +1,1241 @@ +user()->currentAccessToken(); + $service->makeHidden([ + 'id', + ]); + if ($token->can('view:sensitive')) { + return serializeApiResponse($service); + } + + $service->makeHidden([ + 'docker_compose_raw', + 'docker_compose', + ]); + + return serializeApiResponse($service); + } + + #[OA\Get( + summary: 'List', + description: 'List all services.', + path: '/services', + operationId: 'list-services', + security: [ + ['bearerAuth' => []], + ], + tags: ['Services'], + responses: [ + new OA\Response( + response: 200, + description: 'Get all services', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/Service') + ) + ), + ] + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function services(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $projects = Project::where('team_id', $teamId)->get(); + $services = collect(); + foreach ($projects as $project) { + $services->push($project->services()->get()); + } + foreach ($services as $service) { + $service = $this->removeSensitiveData($service); + } + + return response()->json($services->flatten()); + } + + #[OA\Post( + summary: 'Create', + description: 'Create a one-click service', + path: '/services', + operationId: 'create-service', + security: [ + ['bearerAuth' => []], + ], + tags: ['Services'], + requestBody: new OA\RequestBody( + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['server_uuid', 'project_uuid', 'environment_name', 'type'], + properties: [ + 'type' => [ + 'description' => 'The one-click service type', + 'type' => 'string', + 'enum' => [ + 'activepieces', + 'appsmith', + 'appwrite', + 'authentik', + 'babybuddy', + 'budge', + 'changedetection', + 'chatwoot', + 'classicpress-with-mariadb', + 'classicpress-with-mysql', + 'classicpress-without-database', + 'cloudflared', + 'code-server', + 'dashboard', + 'directus', + 'directus-with-postgresql', + 'docker-registry', + 'docuseal', + 'docuseal-with-postgres', + 'dokuwiki', + 'duplicati', + 'emby', + 'embystat', + 'fider', + 'filebrowser', + 'firefly', + 'formbricks', + 'ghost', + 'gitea', + 'gitea-with-mariadb', + 'gitea-with-mysql', + 'gitea-with-postgresql', + 'glance', + 'glances', + 'glitchtip', + 'grafana', + 'grafana-with-postgresql', + 'grocy', + 'heimdall', + 'homepage', + 'jellyfin', + 'kuzzle', + 'listmonk', + 'logto', + 'mediawiki', + 'meilisearch', + 'metabase', + 'metube', + 'minio', + 'moodle', + 'n8n', + 'n8n-with-postgresql', + 'next-image-transformation', + 'nextcloud', + 'nocodb', + 'odoo', + 'openblocks', + 'pairdrop', + 'penpot', + 'phpmyadmin', + 'pocketbase', + 'posthog', + 'reactive-resume', + 'rocketchat', + 'shlink', + 'slash', + 'snapdrop', + 'statusnook', + 'stirling-pdf', + 'supabase', + 'syncthing', + 'tolgee', + 'trigger', + 'trigger-with-external-database', + 'twenty', + 'umami', + 'unleash-with-postgresql', + 'unleash-without-database', + 'uptime-kuma', + 'vaultwarden', + 'vikunja', + 'weblate', + 'whoogle', + 'wordpress-with-mariadb', + 'wordpress-with-mysql', + 'wordpress-without-database', + ], + ], + 'name' => ['type' => 'string', 'maxLength' => 255, 'description' => 'Name of the service.'], + 'description' => ['type' => 'string', 'nullable' => true, 'description' => 'Description of the service.'], + 'project_uuid' => ['type' => 'string', 'description' => 'Project UUID.'], + 'environment_name' => ['type' => 'string', 'description' => 'Environment name.'], + 'server_uuid' => ['type' => 'string', 'description' => 'Server UUID.'], + 'destination_uuid' => ['type' => 'string', 'description' => 'Destination UUID. Required if server has multiple destinations.'], + 'instant_deploy' => ['type' => 'boolean', 'default' => false, 'description' => 'Start the service immediately after creation.'], + ], + ), + ), + ), + responses: [ + new OA\Response( + response: 201, + description: 'Create a service.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'uuid' => ['type' => 'string', 'description' => 'Service UUID.'], + 'domains' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Service domains.'], + ] + ) + ), + ] + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_service(Request $request) + { + $allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy']; + + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'type' => 'string|required', + 'project_uuid' => 'string|required', + 'environment_name' => 'string|required', + 'server_uuid' => 'string|required', + 'destination_uuid' => 'string', + 'name' => 'string|max:255', + 'description' => 'string|nullable', + 'instant_deploy' => 'boolean', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $serverUuid = $request->server_uuid; + $instantDeploy = $request->instant_deploy ?? false; + if ($request->is_public && ! $request->public_port) { + $request->offsetSet('is_public', false); + } + $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); + if (! $project) { + return response()->json(['message' => 'Project not found.'], 404); + } + $environment = $project->environments()->where('name', $request->environment_name)->first(); + if (! $environment) { + return response()->json(['message' => 'Environment not found.'], 404); + } + $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first(); + if (! $server) { + return response()->json(['message' => 'Server not found.'], 404); + } + $destinations = $server->destinations(); + if ($destinations->count() == 0) { + return response()->json(['message' => 'Server has no destinations.'], 400); + } + if ($destinations->count() > 1 && ! $request->has('destination_uuid')) { + return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400); + } + $destination = $destinations->first(); + $services = get_service_templates(); + $serviceKeys = $services->keys(); + if ($serviceKeys->contains($request->type)) { + $oneClickServiceName = $request->type; + $oneClickService = data_get($services, "$oneClickServiceName.compose"); + $oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null); + if ($oneClickDotEnvs) { + $oneClickDotEnvs = str(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/')->filter(function ($value) { + return ! empty($value); + }); + } + if ($oneClickService) { + $service_payload = [ + 'name' => "$oneClickServiceName-".str()->random(10), + 'docker_compose_raw' => base64_decode($oneClickService), + 'environment_id' => $environment->id, + 'service_type' => $oneClickServiceName, + 'server_id' => $server->id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + ]; + if ($oneClickServiceName === 'cloudflared') { + data_set($service_payload, 'connect_to_docker_network', true); + } + $service = Service::create($service_payload); + $service->name = "$oneClickServiceName-".$service->uuid; + $service->save(); + if ($oneClickDotEnvs?->count() > 0) { + $oneClickDotEnvs->each(function ($value) use ($service) { + $key = str()->before($value, '='); + $value = str(str()->after($value, '=')); + $generatedValue = $value; + if ($value->contains('SERVICE_')) { + $command = $value->after('SERVICE_')->beforeLast('_'); + $generatedValue = generateEnvValue($command->value(), $service); + } + EnvironmentVariable::create([ + 'key' => $key, + 'value' => $generatedValue, + 'service_id' => $service->id, + 'is_build_time' => false, + 'is_preview' => false, + ]); + }); + } + $service->parse(isNew: true); + if ($instantDeploy) { + StartService::dispatch($service)->onQueue('high'); + } + $domains = $service->applications()->get()->pluck('fqdn')->sort(); + $domains = $domains->map(function ($domain) { + return str($domain)->beforeLast(':')->value(); + }); + + return response()->json([ + 'uuid' => $service->uuid, + 'domains' => $domains, + ]); + } + + return response()->json(['message' => 'Service not found.'], 404); + } else { + return response()->json(['message' => 'Invalid service type.', 'valid_service_types' => $serviceKeys], 400); + } + + return response()->json(['message' => 'Invalid service type.'], 400); + } + + #[OA\Get( + summary: 'Get', + description: 'Get service by UUID.', + path: '/services/{uuid}', + operationId: 'get-service-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Services'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Service UUID', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Get a service by UUID.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + ref: '#/components/schemas/Service' + ) + ), + ] + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function service_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['message' => 'UUID is required.'], 404); + } + $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); + if (! $service) { + return response()->json(['message' => 'Service not found.'], 404); + } + + $service = $service->load(['applications', 'databases']); + + return response()->json($this->removeSensitiveData($service)); + } + + #[OA\Delete( + summary: 'Delete', + description: 'Delete service by UUID.', + path: '/services/{uuid}', + operationId: 'delete-service-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Services'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Service UUID', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)), + new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Delete a service by UUID', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Service deletion request queued.'], + ], + ) + ), + ] + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function delete_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['message' => 'UUID is required.'], 404); + } + $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); + if (! $service) { + return response()->json(['message' => 'Service not found.'], 404); + } + + DeleteResourceJob::dispatch( + resource: $service, + deleteConfigurations: $request->query->get('delete_configurations', true), + deleteVolumes: $request->query->get('delete_volumes', true), + dockerCleanup: $request->query->get('docker_cleanup', true), + deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) + )->onQueue('high'); + + return response()->json([ + 'message' => 'Service deletion request queued.', + ]); + } + + #[OA\Get( + summary: 'List Envs', + description: 'List all envs by service UUID.', + path: '/services/{uuid}/envs', + operationId: 'list-envs-by-service-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Services'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the service.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'All environment variables by service UUID.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable') + ) + ), + ] + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function envs(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); + if (! $service) { + return response()->json(['message' => 'Service not found.'], 404); + } + + $envs = $service->environment_variables->map(function ($env) { + $env->makeHidden([ + 'application_id', + 'standalone_clickhouse_id', + 'standalone_dragonfly_id', + 'standalone_keydb_id', + 'standalone_mariadb_id', + 'standalone_mongodb_id', + 'standalone_mysql_id', + 'standalone_postgresql_id', + 'standalone_redis_id', + ]); + + return $this->removeSensitiveData($env); + }); + + return response()->json($envs); + } + + #[OA\Patch( + summary: 'Update Env', + description: 'Update env by service UUID.', + path: '/services/{uuid}/envs', + operationId: 'update-env-by-service-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Services'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the service.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + requestBody: new OA\RequestBody( + description: 'Env updated.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['key', 'value'], + properties: [ + 'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'], + 'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'], + 'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'], + 'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'], + 'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'], + 'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'], + 'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'], + ], + ), + ), + ], + ), + responses: [ + new OA\Response( + response: 201, + description: 'Environment variable updated.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Environment variable updated.'], + ] + ) + ), + ] + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function update_env_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); + if (! $service) { + return response()->json(['message' => 'Service not found.'], 404); + } + + $validator = customApiValidator($request->all(), [ + 'key' => 'string|required', + 'value' => 'string|nullable', + 'is_build_time' => 'boolean', + 'is_literal' => 'boolean', + 'is_multiline' => 'boolean', + 'is_shown_once' => 'boolean', + ]); + + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + + $env = $service->environment_variables()->where('key', $request->key)->first(); + if (! $env) { + return response()->json(['message' => 'Environment variable not found.'], 404); + } + + $env->fill($request->all()); + $env->save(); + + return response()->json($this->removeSensitiveData($env))->setStatusCode(201); + } + + #[OA\Patch( + summary: 'Update Envs (Bulk)', + description: 'Update multiple envs by service UUID.', + path: '/services/{uuid}/envs/bulk', + operationId: 'update-envs-by-service-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Services'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the service.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + requestBody: new OA\RequestBody( + description: 'Bulk envs updated.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['data'], + properties: [ + 'data' => [ + 'type' => 'array', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'], + 'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'], + 'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'], + 'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'], + 'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'], + 'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'], + 'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'], + ], + ), + ], + ], + ), + ), + ], + ), + responses: [ + new OA\Response( + response: 201, + description: 'Environment variables updated.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Environment variables updated.'], + ] + ) + ), + ] + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function create_bulk_envs(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); + if (! $service) { + return response()->json(['message' => 'Service not found.'], 404); + } + + $bulk_data = $request->get('data'); + if (! $bulk_data) { + return response()->json(['message' => 'Bulk data is required.'], 400); + } + + $updatedEnvs = collect(); + foreach ($bulk_data as $item) { + $validator = customApiValidator($item, [ + 'key' => 'string|required', + 'value' => 'string|nullable', + 'is_build_time' => 'boolean', + 'is_literal' => 'boolean', + 'is_multiline' => 'boolean', + 'is_shown_once' => 'boolean', + ]); + + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + + $env = $service->environment_variables()->updateOrCreate( + ['key' => $item['key']], + $item + ); + + $updatedEnvs->push($this->removeSensitiveData($env)); + } + + return response()->json($updatedEnvs)->setStatusCode(201); + } + + #[OA\Post( + summary: 'Create Env', + description: 'Create env by service UUID.', + path: '/services/{uuid}/envs', + operationId: 'create-env-by-service-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Services'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the service.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + requestBody: new OA\RequestBody( + required: true, + description: 'Env created.', + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'], + 'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'], + 'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'], + 'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'], + 'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'], + 'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'], + 'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'], + ], + ), + ), + ), + responses: [ + new OA\Response( + response: 201, + description: 'Environment variable created.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'uuid' => ['type' => 'string', 'example' => 'nc0k04gk8g0cgsk440g0koko'], + ] + ) + ), + ] + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function create_env(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); + if (! $service) { + return response()->json(['message' => 'Service not found.'], 404); + } + + $validator = customApiValidator($request->all(), [ + 'key' => 'string|required', + 'value' => 'string|nullable', + 'is_build_time' => 'boolean', + 'is_literal' => 'boolean', + 'is_multiline' => 'boolean', + 'is_shown_once' => 'boolean', + ]); + + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + + $existingEnv = $service->environment_variables()->where('key', $request->key)->first(); + if ($existingEnv) { + return response()->json([ + 'message' => 'Environment variable already exists. Use PATCH request to update it.', + ], 409); + } + + $env = $service->environment_variables()->create($request->all()); + + return response()->json($this->removeSensitiveData($env))->setStatusCode(201); + } + + #[OA\Delete( + summary: 'Delete Env', + description: 'Delete env by UUID.', + path: '/services/{uuid}/envs/{env_uuid}', + operationId: 'delete-env-by-service-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Services'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the service.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + new OA\Parameter( + name: 'env_uuid', + in: 'path', + description: 'UUID of the environment variable.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Environment variable deleted.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Environment variable deleted.'], + ] + ) + ), + ] + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function delete_env_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); + if (! $service) { + return response()->json(['message' => 'Service not found.'], 404); + } + + $env = EnvironmentVariable::where('uuid', $request->env_uuid) + ->where('service_id', $service->id) + ->first(); + + if (! $env) { + return response()->json(['message' => 'Environment variable not found.'], 404); + } + + $env->forceDelete(); + + return response()->json(['message' => 'Environment variable deleted.']); + } + + #[OA\Get( + summary: 'Start', + description: 'Start service. `Post` request is also accepted.', + path: '/services/{uuid}/start', + operationId: 'start-service-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Services'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the service.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Start service.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Service starting request queued.'], + ] + ) + ), + ] + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function action_deploy(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); + if (! $service) { + return response()->json(['message' => 'Service not found.'], 404); + } + if (str($service->status())->contains('running')) { + return response()->json(['message' => 'Service is already running.'], 400); + } + StartService::dispatch($service)->onQueue('high'); + + return response()->json( + [ + 'message' => 'Service starting request queued.', + ], + 200 + ); + } + + #[OA\Get( + summary: 'Stop', + description: 'Stop service. `Post` request is also accepted.', + path: '/services/{uuid}/stop', + operationId: 'stop-service-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Services'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the service.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Stop service.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Service stopping request queued.'], + ] + ) + ), + ] + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function action_stop(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); + if (! $service) { + return response()->json(['message' => 'Service not found.'], 404); + } + if (str($service->status())->contains('stopped') || str($service->status())->contains('exited')) { + return response()->json(['message' => 'Service is already stopped.'], 400); + } + StopService::dispatch($service)->onQueue('high'); + + return response()->json( + [ + 'message' => 'Service stopping request queued.', + ], + 200 + ); + } + + #[OA\Get( + summary: 'Restart', + description: 'Restart service. `Post` request is also accepted.', + path: '/services/{uuid}/restart', + operationId: 'restart-service-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Services'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the service.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Restart service.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Service restaring request queued.'], + ] + ) + ), + ] + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function action_restart(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); + if (! $service) { + return response()->json(['message' => 'Service not found.'], 404); + } + RestartService::dispatch($service)->onQueue('high'); + + return response()->json( + [ + 'message' => 'Service restarting request queued.', + ], + 200 + ); + } +} diff --git a/app/Http/Controllers/Api/Team.php b/app/Http/Controllers/Api/Team.php deleted file mode 100644 index c895f2c1b..000000000 --- a/app/Http/Controllers/Api/Team.php +++ /dev/null @@ -1,74 +0,0 @@ -user()->teams; - - return response()->json($teams); - } - - public function team_by_id(Request $request) - { - $id = $request->id; - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $teams = auth()->user()->teams; - $team = $teams->where('id', $id)->first(); - if (is_null($team)) { - return response()->json(['error' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid'], 404); - } - - return response()->json($team); - } - - public function members_by_id(Request $request) - { - $id = $request->id; - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $teams = auth()->user()->teams; - $team = $teams->where('id', $id)->first(); - if (is_null($team)) { - return response()->json(['error' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid-members'], 404); - } - - return response()->json($team->members); - } - - public function current_team(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $team = auth()->user()->currentTeam(); - - return response()->json($team); - } - - public function current_team_members(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $team = auth()->user()->currentTeam(); - - return response()->json($team->members); - } -} diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php new file mode 100644 index 000000000..3f951c6f7 --- /dev/null +++ b/app/Http/Controllers/Api/TeamController.php @@ -0,0 +1,275 @@ +user()->currentAccessToken(); + $team->makeHidden([ + 'custom_server_limit', + 'pivot', + ]); + if ($token->can('view:sensitive')) { + return serializeApiResponse($team); + } + $team->makeHidden([ + 'smtp_username', + 'smtp_password', + 'resend_api_key', + 'telegram_token', + ]); + + return serializeApiResponse($team); + } + + #[OA\Get( + summary: 'List', + description: 'Get all teams.', + path: '/teams', + operationId: 'list-teams', + security: [ + ['bearerAuth' => []], + ], + tags: ['Teams'], + responses: [ + new OA\Response( + response: 200, + description: 'List of teams.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/Team') + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function teams(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $teams = auth()->user()->teams->sortBy('id'); + $teams = $teams->map(function ($team) { + return $this->removeSensitiveData($team); + }); + + return response()->json( + $teams, + ); + } + + #[OA\Get( + summary: 'Get', + description: 'Get team by TeamId.', + path: '/teams/{id}', + operationId: 'get-team-by-id', + security: [ + ['bearerAuth' => []], + ], + tags: ['Teams'], + parameters: [ + new OA\Parameter(name: 'id', in: 'path', required: true, description: 'Team ID', schema: new OA\Schema(type: 'integer')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'List of teams.', + content: new OA\JsonContent(ref: '#/components/schemas/Team') + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function team_by_id(Request $request) + { + $id = $request->id; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $teams = auth()->user()->teams; + $team = $teams->where('id', $id)->first(); + if (is_null($team)) { + return response()->json(['message' => 'Team not found.'], 404); + } + $team = $this->removeSensitiveData($team); + + return response()->json( + serializeApiResponse($team), + ); + } + + #[OA\Get( + summary: 'Members', + description: 'Get members by TeamId.', + path: '/teams/{id}/members', + operationId: 'get-members-by-team-id', + security: [ + ['bearerAuth' => []], + ], + tags: ['Teams'], + parameters: [ + new OA\Parameter(name: 'id', in: 'path', required: true, description: 'Team ID', schema: new OA\Schema(type: 'integer')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'List of members.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/User') + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function members_by_id(Request $request) + { + $id = $request->id; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $teams = auth()->user()->teams; + $team = $teams->where('id', $id)->first(); + if (is_null($team)) { + return response()->json(['message' => 'Team not found.'], 404); + } + $members = $team->members; + $members->makeHidden([ + 'pivot', + ]); + + return response()->json( + serializeApiResponse($members), + ); + } + + #[OA\Get( + summary: 'Authenticated Team', + description: 'Get currently authenticated team.', + path: '/teams/current', + operationId: 'get-current-team', + security: [ + ['bearerAuth' => []], + ], + tags: ['Teams'], + responses: [ + new OA\Response( + response: 200, + description: 'Current Team.', + content: new OA\JsonContent(ref: '#/components/schemas/Team')), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function current_team(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $team = auth()->user()->currentTeam(); + + return response()->json( + $this->removeSensitiveData($team), + ); + } + + #[OA\Get( + summary: 'Authenticated Team Members', + description: 'Get currently authenticated team members.', + path: '/teams/current/members', + operationId: 'get-current-team-members', + security: [ + ['bearerAuth' => []], + ], + tags: ['Teams'], + responses: [ + new OA\Response( + response: 200, + description: 'Currently authenticated team members.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/User') + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function current_team_members(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $team = auth()->user()->currentTeam(); + $team->members->makeHidden([ + 'pivot', + ]); + + return response()->json( + serializeApiResponse($team->members), + ); + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 3363d8164..9f1e4eeb8 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -81,8 +81,8 @@ class Controller extends BaseController $token = request()->get('token'); if ($token) { $decrypted = Crypt::decryptString($token); - $email = Str::of($decrypted)->before('@@@'); - $password = Str::of($decrypted)->after('@@@'); + $email = str($decrypted)->before('@@@'); + $password = str($decrypted)->after('@@@'); $user = User::whereEmail($email)->first(); if (! $user) { return redirect()->route('login'); @@ -110,59 +110,54 @@ class Controller extends BaseController return redirect()->route('login')->with('error', 'Invalid credentials.'); } - public function accept_invitation() + public function acceptInvitation() { - try { - $resetPassword = request()->query('reset-password'); - $invitationUuid = request()->route('uuid'); - $invitation = TeamInvitation::whereUuid($invitationUuid)->firstOrFail(); - $user = User::whereEmail($invitation->email)->firstOrFail(); - $invitationValid = $invitation->isValid(); - if ($invitationValid) { - if ($resetPassword) { - $user->update([ - 'password' => Hash::make($invitationUuid), - 'force_password_reset' => true, - ]); - } - if ($user->teams()->where('team_id', $invitation->team->id)->exists()) { - $invitation->delete(); + $resetPassword = request()->query('reset-password'); + $invitationUuid = request()->route('uuid'); - return redirect()->route('team.index'); - } - $user->teams()->attach($invitation->team->id, ['role' => $invitation->role]); + $invitation = TeamInvitation::whereUuid($invitationUuid)->firstOrFail(); + $user = User::whereEmail($invitation->email)->firstOrFail(); + + if (Auth::id() !== $user->id) { + abort(400, 'You are not allowed to accept this invitation.'); + } + $invitationValid = $invitation->isValid(); + + if ($invitationValid) { + if ($resetPassword) { + $user->update([ + 'password' => Hash::make($invitationUuid), + 'force_password_reset' => true, + ]); + } + if ($user->teams()->where('team_id', $invitation->team->id)->exists()) { $invitation->delete(); - if (auth()->user()?->id !== $user->id) { - return redirect()->route('login'); - } - refreshSession($invitation->team); return redirect()->route('team.index'); - } else { - abort(401); } - } catch (\Throwable $e) { - ray($e->getMessage()); - throw $e; + $user->teams()->attach($invitation->team->id, ['role' => $invitation->role]); + $invitation->delete(); + + refreshSession($invitation->team); + + return redirect()->route('team.index'); + } else { + abort(400, 'Invitation expired.'); } } public function revoke_invitation() { - try { - $invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail(); - $user = User::whereEmail($invitation->email)->firstOrFail(); - if (is_null(auth()->user())) { - return redirect()->route('login'); - } - if (auth()->user()->id !== $user->id) { - abort(401); - } - $invitation->delete(); - - return redirect()->route('team.index'); - } catch (\Throwable $e) { - throw $e; + $invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail(); + $user = User::whereEmail($invitation->email)->firstOrFail(); + if (is_null(Auth::user())) { + return redirect()->route('login'); } + if (Auth::id() !== $user->id) { + abort(401); + } + $invitation->delete(); + + return redirect()->route('team.index'); } } diff --git a/app/Http/Controllers/OauthController.php b/app/Http/Controllers/OauthController.php index 5b17fe926..3a3f18c9c 100644 --- a/app/Http/Controllers/OauthController.php +++ b/app/Http/Controllers/OauthController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers; use App\Models\User; use Illuminate\Support\Facades\Auth; +use Symfony\Component\HttpKernel\Exception\HttpException; class OauthController extends Controller { @@ -20,6 +21,11 @@ class OauthController extends Controller $oauthUser = get_socialite_provider($provider)->user(); $user = User::whereEmail($oauthUser->email)->first(); if (! $user) { + $settings = instanceSettings(); + if (! $settings->is_registration_enabled) { + abort(403, 'Registration is disabled'); + } + $user = User::create([ 'name' => $oauthUser->name, 'email' => $oauthUser->email, @@ -29,9 +35,9 @@ class OauthController extends Controller return redirect('/'); } catch (\Exception $e) { - ray($e->getMessage()); + $errorCode = $e instanceof HttpException ? 'auth.failed' : 'auth.failed.callback'; - return redirect()->route('login')->withErrors([__('auth.failed.callback')]); + return redirect()->route('login')->withErrors([__($errorCode)]); } } } diff --git a/app/Http/Controllers/UploadController.php b/app/Http/Controllers/UploadController.php index 8e52fda32..4d34a1000 100644 --- a/app/Http/Controllers/UploadController.php +++ b/app/Http/Controllers/UploadController.php @@ -5,7 +5,6 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Http\UploadedFile; use Illuminate\Routing\Controller as BaseController; -use Illuminate\Support\Facades\Storage; use Pion\Laravel\ChunkUpload\Exceptions\UploadMissingFileException; use Pion\Laravel\ChunkUpload\Handler\HandlerFactory; use Pion\Laravel\ChunkUpload\Receiver\FileReceiver; @@ -21,7 +20,7 @@ class UploadController extends BaseController $receiver = new FileReceiver('file', $request, HandlerFactory::classFromRequest($request)); if ($receiver->isUploaded() === false) { - throw new UploadMissingFileException(); + throw new UploadMissingFileException; } $save = $receiver->receive(); diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php index b9035b755..8c74f95e5 100644 --- a/app/Http/Controllers/Webhook/Bitbucket.php +++ b/app/Http/Controllers/Webhook/Bitbucket.php @@ -16,7 +16,6 @@ class Bitbucket extends Controller { try { if (app()->isDownForMaintenance()) { - ray('Maintenance mode is on'); $epoch = now()->valueOf(); $data = [ 'attributes' => $request->attributes->all(), @@ -55,7 +54,6 @@ class Bitbucket extends Controller 'message' => 'Nothing to do. No branch found in the request.', ]); } - ray('Manual webhook bitbucket push event with branch: '.$branch); } if ($x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') { $branch = data_get($payload, 'pullrequest.destination.branch.name'); @@ -85,7 +83,6 @@ class Bitbucket extends Controller 'status' => 'failed', 'message' => 'Invalid signature.', ]); - ray('Invalid signature'); continue; } @@ -96,14 +93,12 @@ class Bitbucket extends Controller 'status' => 'failed', 'message' => 'Server is not functional.', ]); - ray('Server is not functional: '.$application->destination->server->name); continue; } if ($x_bitbucket_event === 'repo:push') { if ($application->isDeployable()) { - ray('Deploying '.$application->name.' with branch '.$branch); - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, @@ -126,16 +121,26 @@ class Bitbucket extends Controller } if ($x_bitbucket_event === 'pullrequest:created') { if ($application->isPRDeployable()) { - ray('Deploying preview for '.$application->name.' with branch '.$branch.' and base branch '.$base_branch.' and pull request id '.$pull_request_id); - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { - ApplicationPreview::create([ - 'git_type' => 'bitbucket', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, - ]); + if ($application->build_pack === 'dockercompose') { + $pr_app = ApplicationPreview::create([ + 'git_type' => 'bitbucket', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + 'docker_compose_domains' => $application->docker_compose_domains, + ]); + $pr_app->generate_preview_fqdn_compose(); + } else { + ApplicationPreview::create([ + 'git_type' => 'bitbucket', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } } queue_application_deployment( application: $application, @@ -160,7 +165,6 @@ class Bitbucket extends Controller } } if ($x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') { - ray('Pull request rejected'); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if ($found) { $found->delete(); @@ -180,12 +184,9 @@ class Bitbucket extends Controller } } } - ray($return_payloads); return response($return_payloads); } catch (Exception $e) { - ray($e); - return handleError($e); } } diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php index 388481949..cc53f2034 100644 --- a/app/Http/Controllers/Webhook/Gitea.php +++ b/app/Http/Controllers/Webhook/Gitea.php @@ -19,15 +19,12 @@ class Gitea extends Controller $return_payloads = collect([]); $x_gitea_delivery = request()->header('X-Gitea-Delivery'); if (app()->isDownForMaintenance()) { - ray('Maintenance mode is on'); $epoch = now()->valueOf(); $files = Storage::disk('webhooks-during-maintenance')->files(); $gitea_delivery_found = collect($files)->filter(function ($file) use ($x_gitea_delivery) { return Str::contains($file, $x_gitea_delivery); })->first(); if ($gitea_delivery_found) { - ray('Webhook already found'); - return; } $data = [ @@ -67,8 +64,6 @@ class Gitea extends Controller $removed_files = data_get($payload, 'commits.*.removed'); $modified_files = data_get($payload, 'commits.*.modified'); $changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten(); - ray($changed_files); - ray('Manual Webhook Gitea Push Event with branch: '.$branch); } if ($x_gitea_event === 'pull_request') { $action = data_get($payload, 'action'); @@ -77,7 +72,6 @@ class Gitea extends Controller $pull_request_html_url = data_get($payload, 'pull_request.html_url'); $branch = data_get($payload, 'pull_request.head.ref'); $base_branch = data_get($payload, 'pull_request.base.ref'); - ray('Webhook Gitea Pull Request Event with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id); } if (! $branch) { return response('Nothing to do. No branch found in the request.'); @@ -99,7 +93,6 @@ class Gitea extends Controller $webhook_secret = data_get($application, 'manual_webhook_secret_gitea'); $hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret); if (! hash_equals($x_hub_signature_256, $hmac) && ! isDev()) { - ray('Invalid signature'); $return_payloads->push([ 'application' => $application->name, 'status' => 'failed', @@ -122,8 +115,7 @@ class Gitea extends Controller if ($application->isDeployable()) { $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { - ray('Deploying '.$application->name.' with branch '.$branch); - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, @@ -162,15 +154,26 @@ class Gitea extends Controller if ($x_gitea_event === 'pull_request') { if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { if ($application->isPRDeployable()) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { - ApplicationPreview::create([ - 'git_type' => 'gitea', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, - ]); + if ($application->build_pack === 'dockercompose') { + $pr_app = ApplicationPreview::create([ + 'git_type' => 'gitea', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + 'docker_compose_domains' => $application->docker_compose_domains, + ]); + $pr_app->generate_preview_fqdn_compose(); + } else { + ApplicationPreview::create([ + 'git_type' => 'gitea', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } } queue_application_deployment( application: $application, @@ -216,12 +219,9 @@ class Gitea extends Controller } } } - ray($return_payloads); return response($return_payloads); } catch (Exception $e) { - ray($e->getMessage()); - return handleError($e); } } diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index 403438193..3683adaa8 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -25,15 +25,12 @@ class Github extends Controller $return_payloads = collect([]); $x_github_delivery = request()->header('X-GitHub-Delivery'); if (app()->isDownForMaintenance()) { - ray('Maintenance mode is on'); $epoch = now()->valueOf(); $files = Storage::disk('webhooks-during-maintenance')->files(); $github_delivery_found = collect($files)->filter(function ($file) use ($x_github_delivery) { return Str::contains($file, $x_github_delivery); })->first(); if ($github_delivery_found) { - ray('Webhook already found'); - return; } $data = [ @@ -73,7 +70,6 @@ class Github extends Controller $removed_files = data_get($payload, 'commits.*.removed'); $modified_files = data_get($payload, 'commits.*.modified'); $changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten(); - ray('Manual Webhook GitHub Push Event with branch: '.$branch); } if ($x_github_event === 'pull_request') { $action = data_get($payload, 'action'); @@ -82,7 +78,6 @@ class Github extends Controller $pull_request_html_url = data_get($payload, 'pull_request.html_url'); $branch = data_get($payload, 'pull_request.head.ref'); $base_branch = data_get($payload, 'pull_request.base.ref'); - ray('Webhook GitHub Pull Request Event with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id); } if (! $branch) { return response('Nothing to do. No branch found in the request.'); @@ -104,7 +99,6 @@ class Github extends Controller $webhook_secret = data_get($application, 'manual_webhook_secret_github'); $hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret); if (! hash_equals($x_hub_signature_256, $hmac) && ! isDev()) { - ray('Invalid signature'); $return_payloads->push([ 'application' => $application->name, 'status' => 'failed', @@ -127,8 +121,7 @@ class Github extends Controller if ($application->isDeployable()) { $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { - ray('Deploying '.$application->name.' with branch '.$branch); - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, @@ -167,15 +160,26 @@ class Github extends Controller if ($x_github_event === 'pull_request') { if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { if ($application->isPRDeployable()) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { - ApplicationPreview::create([ - 'git_type' => 'github', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, - ]); + if ($application->build_pack === 'dockercompose') { + $pr_app = ApplicationPreview::create([ + 'git_type' => 'github', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + 'docker_compose_domains' => $application->docker_compose_domains, + ]); + $pr_app->generate_preview_fqdn_compose(); + } else { + ApplicationPreview::create([ + 'git_type' => 'github', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } } queue_application_deployment( application: $application, @@ -221,12 +225,9 @@ class Github extends Controller } } } - ray($return_payloads); return response($return_payloads); } catch (Exception $e) { - ray($e->getMessage()); - return handleError($e); } } @@ -238,15 +239,12 @@ class Github extends Controller $id = null; $x_github_delivery = $request->header('X-GitHub-Delivery'); if (app()->isDownForMaintenance()) { - ray('Maintenance mode is on'); $epoch = now()->valueOf(); $files = Storage::disk('webhooks-during-maintenance')->files(); $github_delivery_found = collect($files)->filter(function ($file) use ($x_github_delivery) { return Str::contains($file, $x_github_delivery); })->first(); if ($github_delivery_found) { - ray('Webhook already found'); - return; } $data = [ @@ -302,7 +300,6 @@ class Github extends Controller $removed_files = data_get($payload, 'commits.*.removed'); $modified_files = data_get($payload, 'commits.*.modified'); $changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten(); - ray('Webhook GitHub Push Event: '.$id.' with branch: '.$branch); } if ($x_github_event === 'pull_request') { $action = data_get($payload, 'action'); @@ -311,7 +308,6 @@ class Github extends Controller $pull_request_html_url = data_get($payload, 'pull_request.html_url'); $branch = data_get($payload, 'pull_request.head.ref'); $base_branch = data_get($payload, 'pull_request.base.ref'); - ray('Webhook GitHub Pull Request Event: '.$id.' with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id); } if (! $id || ! $branch) { return response('Nothing to do. No id or branch found.'); @@ -329,7 +325,6 @@ class Github extends Controller return response("Nothing to do. No applications found with branch '$base_branch'."); } } - foreach ($applications as $application) { $isFunctional = $application->destination->server->isFunctional(); if (! $isFunctional) { @@ -346,8 +341,7 @@ class Github extends Controller if ($application->isDeployable()) { $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { - ray('Deploying '.$application->name.' with branch '.$branch); - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, @@ -386,7 +380,7 @@ class Github extends Controller if ($x_github_event === 'pull_request') { if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { if ($application->isPRDeployable()) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { ApplicationPreview::create([ @@ -421,8 +415,13 @@ class Github extends Controller if ($action === 'closed' || $action === 'close') { $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if ($found) { - $container_name = generateApplicationContainerName($application, $pull_request_id); - instant_remote_process(["docker rm -f $container_name"], $application->destination->server); + $containers = getCurrentApplicationContainerStatus($application->destination->server, $application->id, $pull_request_id); + if ($containers->isNotEmpty()) { + $containers->each(function ($container) use ($application) { + $container_name = data_get($container, 'Names'); + instant_remote_process(["docker rm -f $container_name"], $application->destination->server); + }); + } ApplicationPullRequestUpdateJob::dispatchSync(application: $application, preview: $found, status: ProcessStatus::CLOSED); $found->delete(); @@ -445,8 +444,6 @@ class Github extends Controller return response($return_payloads); } catch (Exception $e) { - ray($e->getMessage()); - return handleError($e); } } @@ -490,7 +487,6 @@ class Github extends Controller try { $installation_id = $request->get('installation_id'); if (app()->isDownForMaintenance()) { - ray('Maintenance mode is on'); $epoch = now()->valueOf(); $data = [ 'attributes' => $request->attributes->all(), diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php index a3d7712eb..d8dcc0c3b 100644 --- a/app/Http/Controllers/Webhook/Gitlab.php +++ b/app/Http/Controllers/Webhook/Gitlab.php @@ -17,7 +17,6 @@ class Gitlab extends Controller { try { if (app()->isDownForMaintenance()) { - ray('Maintenance mode is on'); $epoch = now()->valueOf(); $data = [ 'attributes' => $request->attributes->all(), @@ -34,6 +33,7 @@ class Gitlab extends Controller return; } + $return_payloads = collect([]); $payload = $request->collect(); $headers = $request->headers->all(); @@ -49,6 +49,15 @@ class Gitlab extends Controller return response($return_payloads); } + if (empty($x_gitlab_token)) { + $return_payloads->push([ + 'status' => 'failed', + 'message' => 'Invalid signature.', + ]); + + return response($return_payloads); + } + if ($x_gitlab_event === 'push') { $branch = data_get($payload, 'ref'); $full_name = data_get($payload, 'project.path_with_namespace'); @@ -67,7 +76,6 @@ class Gitlab extends Controller $removed_files = data_get($payload, 'commits.*.removed'); $modified_files = data_get($payload, 'commits.*.modified'); $changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten(); - ray('Manual Webhook GitLab Push Event with branch: '.$branch); } if ($x_gitlab_event === 'merge_request') { $action = data_get($payload, 'object_attributes.action'); @@ -84,7 +92,6 @@ class Gitlab extends Controller return response($return_payloads); } - ray('Webhook GitHub Pull Request Event with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id); } $applications = Application::where('git_repository', 'like', "%$full_name%"); if ($x_gitlab_event === 'push') { @@ -117,7 +124,6 @@ class Gitlab extends Controller 'status' => 'failed', 'message' => 'Invalid signature.', ]); - ray('Invalid signature'); continue; } @@ -128,7 +134,6 @@ class Gitlab extends Controller 'status' => 'failed', 'message' => 'Server is not functional', ]); - ray('Server is not functional: '.$application->destination->server->name); continue; } @@ -136,8 +141,7 @@ class Gitlab extends Controller if ($application->isDeployable()) { $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { - ray('Deploying '.$application->name.' with branch '.$branch); - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, @@ -171,21 +175,31 @@ class Gitlab extends Controller 'application_uuid' => $application->uuid, 'application_name' => $application->name, ]); - ray('Deployments disabled for '.$application->name); } } if ($x_gitlab_event === 'merge_request') { if ($action === 'open' || $action === 'opened' || $action === 'synchronize' || $action === 'reopened' || $action === 'reopen' || $action === 'update') { if ($application->isPRDeployable()) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { - ApplicationPreview::create([ - 'git_type' => 'gitlab', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, - ]); + if ($application->build_pack === 'dockercompose') { + $pr_app = ApplicationPreview::create([ + 'git_type' => 'gitlab', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + 'docker_compose_domains' => $application->docker_compose_domains, + ]); + $pr_app->generate_preview_fqdn_compose(); + } else { + ApplicationPreview::create([ + 'git_type' => 'gitlab', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } } queue_application_deployment( application: $application, @@ -196,7 +210,6 @@ class Gitlab extends Controller is_webhook: true, git_type: 'gitlab' ); - ray('Deploying preview for '.$application->name.' with branch '.$branch.' and base branch '.$base_branch.' and pull request id '.$pull_request_id); $return_payloads->push([ 'application' => $application->name, 'status' => 'success', @@ -208,7 +221,6 @@ class Gitlab extends Controller 'status' => 'failed', 'message' => 'Preview deployments disabled', ]); - ray('Preview deployments disabled for '.$application->name); } } elseif ($action === 'closed' || $action === 'close' || $action === 'merge') { $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); @@ -242,8 +254,6 @@ class Gitlab extends Controller return response($return_payloads); } catch (Exception $e) { - ray($e->getMessage()); - return handleError($e); } } diff --git a/app/Http/Controllers/Webhook/Stripe.php b/app/Http/Controllers/Webhook/Stripe.php index e404a8ebc..e94209b23 100644 --- a/app/Http/Controllers/Webhook/Stripe.php +++ b/app/Http/Controllers/Webhook/Stripe.php @@ -5,15 +5,12 @@ namespace App\Http\Controllers\Webhook; use App\Http\Controllers\Controller; use App\Jobs\ServerLimitCheckJob; use App\Jobs\SubscriptionInvoiceFailedJob; -use App\Jobs\SubscriptionTrialEndedJob; -use App\Jobs\SubscriptionTrialEndsSoonJob; use App\Models\Subscription; use App\Models\Team; use App\Models\Webhook; use Exception; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; -use Illuminate\Support\Sleep; use Illuminate\Support\Str; class Stripe extends Controller @@ -22,7 +19,6 @@ class Stripe extends Controller { try { if (app()->isDownForMaintenance()) { - ray('Maintenance mode is on'); $epoch = now()->valueOf(); $data = [ 'attributes' => $request->attributes->all(), @@ -54,6 +50,30 @@ class Stripe extends Controller $type = data_get($event, 'type'); $data = data_get($event, 'data.object'); switch ($type) { + case 'radar.early_fraud_warning.created': + $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); + $id = data_get($data, 'id'); + $charge = data_get($data, 'charge'); + if ($charge) { + $stripe->refunds->create(['charge' => $charge]); + } + $pi = data_get($data, 'payment_intent'); + $piData = $stripe->paymentIntents->retrieve($pi, []); + $customerId = data_get($piData, 'customer'); + $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); + if ($subscription) { + $subscriptionId = data_get($subscription, 'stripe_subscription_id'); + $stripe->subscriptions->cancel($subscriptionId, []); + $subscription->update([ + 'stripe_invoice_paid' => false, + ]); + send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}"); + } else { + send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}"); + + return response("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}", 400); + } + break; case 'checkout.session.completed': $clientReferenceId = data_get($data, 'client_reference_id'); if (is_null($clientReferenceId)) { @@ -68,18 +88,19 @@ class Stripe extends Controller $found = $team->members->where('id', $userId)->first(); if (! $found->isAdmin()) { send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}."); - throw new Exception("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}."); + + return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.", 400); } $subscription = Subscription::where('team_id', $teamId)->first(); if ($subscription) { - send_internal_notification('Old subscription activated for team: '.$teamId); + // send_internal_notification('Old subscription activated for team: '.$teamId); $subscription->update([ 'stripe_subscription_id' => $subscriptionId, 'stripe_customer_id' => $customerId, 'stripe_invoice_paid' => true, ]); } else { - send_internal_notification('New subscription for team: '.$teamId); + // send_internal_notification('New subscription for team: '.$teamId); Subscription::create([ 'team_id' => $teamId, 'stripe_subscription_id' => $subscriptionId, @@ -92,90 +113,120 @@ class Stripe extends Controller $customerId = data_get($data, 'customer'); $planId = data_get($data, 'lines.data.0.plan.id'); if (Str::contains($excludedPlans, $planId)) { - send_internal_notification('Subscription excluded.'); + // send_internal_notification('Subscription excluded.'); break; } $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); - if (! $subscription) { - Sleep::for(5)->seconds(); - $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); + if ($subscription) { + $subscription->update([ + 'stripe_invoice_paid' => true, + ]); + } else { + return response("No subscription found for customer: {$customerId}", 400); } - $subscription->update([ - 'stripe_invoice_paid' => true, - ]); break; case 'invoice.payment_failed': $customerId = data_get($data, 'customer'); $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); if (! $subscription) { - send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId); + // send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId); return response('No subscription found in Coolify.'); } $team = data_get($subscription, 'team'); if (! $team) { - send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId); + // send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId); return response('No team found in Coolify.'); } if (! $subscription->stripe_invoice_paid) { SubscriptionInvoiceFailedJob::dispatch($team); - send_internal_notification('Invoice payment failed: '.$customerId); + // send_internal_notification('Invoice payment failed: '.$customerId); } else { - send_internal_notification('Invoice payment failed but already paid: '.$customerId); + // send_internal_notification('Invoice payment failed but already paid: '.$customerId); } break; case 'payment_intent.payment_failed': $customerId = data_get($data, 'customer'); $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); if (! $subscription) { - send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId); + // send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId); return response('No subscription found in Coolify.'); } if ($subscription->stripe_invoice_paid) { - send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId); + // send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId); return; } send_internal_notification('Subscription payment failed for customer: '.$customerId); break; + case 'customer.subscription.created': + $customerId = data_get($data, 'customer'); + $subscriptionId = data_get($data, 'id'); + $teamId = data_get($data, 'metadata.team_id'); + $userId = data_get($data, 'metadata.user_id'); + if (! $teamId || ! $userId) { + $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); + if ($subscription) { + return response("Subscription already exists for customer: {$customerId}", 200); + } + + return response('No team id or user id found', 400); + } + $team = Team::find($teamId); + $found = $team->members->where('id', $userId)->first(); + if (! $found->isAdmin()) { + send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}."); + + return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.", 400); + } + $subscription = Subscription::where('team_id', $teamId)->first(); + if ($subscription) { + return response("Subscription already exists for team: {$teamId}", 200); + } else { + Subscription::create([ + 'team_id' => $teamId, + 'stripe_subscription_id' => $subscriptionId, + 'stripe_customer_id' => $customerId, + 'stripe_invoice_paid' => false, + ]); + + return response('Subscription created'); + } case 'customer.subscription.updated': + $teamId = data_get($data, 'metadata.team_id'); + $userId = data_get($data, 'metadata.user_id'); $customerId = data_get($data, 'customer'); $status = data_get($data, 'status'); $subscriptionId = data_get($data, 'items.data.0.subscription'); $planId = data_get($data, 'items.data.0.plan.id'); if (Str::contains($excludedPlans, $planId)) { - send_internal_notification('Subscription excluded.'); + // send_internal_notification('Subscription excluded.'); break; } $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); - if (! $subscription) { - Sleep::for(5)->seconds(); - $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); - } if (! $subscription) { if ($status === 'incomplete_expired') { - send_internal_notification('Subscription incomplete expired for customer: '.$customerId); - return response('Subscription incomplete expired', 200); } - send_internal_notification('No subscription found for: '.$customerId); - - return response('No subscription found', 400); + if ($teamId) { + $subscription = Subscription::create([ + 'team_id' => $teamId, + 'stripe_subscription_id' => $subscriptionId, + 'stripe_customer_id' => $customerId, + 'stripe_invoice_paid' => false, + ]); + } else { + return response('No subscription and team id found', 400); + } } - $trialEndedAlready = data_get($subscription, 'stripe_trial_already_ended'); $cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end'); - $alreadyCancelAtPeriodEnd = data_get($subscription, 'stripe_cancel_at_period_end'); $feedback = data_get($data, 'cancellation_details.feedback'); $comment = data_get($data, 'cancellation_details.comment'); $lookup_key = data_get($data, 'items.data.0.price.lookup_key'); - if (str($lookup_key)->contains('ultimate') || str($lookup_key)->contains('dynamic')) { - if (str($lookup_key)->contains('dynamic')) { - $quantity = data_get($data, 'items.data.0.quantity', 2); - } else { - $quantity = data_get($data, 'items.data.0.quantity', 10); - } + if (str($lookup_key)->contains('dynamic')) { + $quantity = data_get($data, 'items.data.0.quantity', 2); $team = data_get($subscription, 'team'); if ($team) { $team->update([ @@ -194,28 +245,12 @@ class Stripe extends Controller $subscription->update([ 'stripe_invoice_paid' => false, ]); - send_internal_notification('Subscription paused or incomplete for customer: '.$customerId); } - - // Trial ended but subscribed, reactive servers - if ($trialEndedAlready && $status === 'active') { - $team = data_get($subscription, 'team'); - $team->trialEndedButSubscribed(); - } - if ($feedback) { $reason = "Cancellation feedback for {$customerId}: '".$feedback."'"; if ($comment) { $reason .= ' with comment: \''.$comment."'"; } - send_internal_notification($reason); - } - if ($alreadyCancelAtPeriodEnd !== $cancelAtPeriodEnd) { - if ($cancelAtPeriodEnd) { - // send_internal_notification('Subscription cancelled at period end for team: ' . $subscription->team->id); - } else { - send_internal_notification('customer.subscription.updated for customer: '.$customerId); - } } break; case 'customer.subscription.deleted': @@ -223,42 +258,7 @@ class Stripe extends Controller $customerId = data_get($data, 'customer'); $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); $team = data_get($subscription, 'team'); - if ($team) { - $team->trialEnded(); - } - $subscription->update([ - 'stripe_subscription_id' => null, - 'stripe_plan_id' => null, - 'stripe_cancel_at_period_end' => false, - 'stripe_invoice_paid' => false, - 'stripe_trial_already_ended' => true, - ]); - send_internal_notification('customer.subscription.deleted for customer: '.$customerId); - break; - case 'customer.subscription.trial_will_end': - // Not used for now - $customerId = data_get($data, 'customer'); - $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); - $team = data_get($subscription, 'team'); - if (! $team) { - throw new Exception('No team found for subscription: '.$subscription->id); - } - SubscriptionTrialEndsSoonJob::dispatch($team); - break; - case 'customer.subscription.paused': - $customerId = data_get($data, 'customer'); - $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); - $team = data_get($subscription, 'team'); - if (! $team) { - throw new Exception('No team found for subscription: '.$subscription->id); - } - $team->trialEnded(); - $subscription->update([ - 'stripe_trial_already_ended' => true, - 'stripe_invoice_paid' => false, - ]); - SubscriptionTrialEndedJob::dispatch($team); - send_internal_notification('Subscription paused for customer: '.$customerId); + $team?->subscriptionEnded(); break; default: // Unhandled event type diff --git a/app/Http/Controllers/Webhook/Waitlist.php b/app/Http/Controllers/Webhook/Waitlist.php index ea635836c..dec8ca72d 100644 --- a/app/Http/Controllers/Webhook/Waitlist.php +++ b/app/Http/Controllers/Webhook/Waitlist.php @@ -13,7 +13,6 @@ class Waitlist extends Controller { $email = request()->get('email'); $confirmation_code = request()->get('confirmation_code'); - ray($email, $confirmation_code); try { $found = ModelsWaitlist::where('uuid', $confirmation_code)->where('email', $email)->first(); if ($found) { @@ -36,7 +35,6 @@ class Waitlist extends Controller return redirect()->route('dashboard'); } catch (Exception $e) { send_internal_notification('Waitlist confirmation failed: '.$e->getMessage()); - ray($e->getMessage()); return redirect()->route('dashboard'); } @@ -58,7 +56,6 @@ class Waitlist extends Controller return redirect()->route('dashboard'); } catch (Exception $e) { send_internal_notification('Waitlist cancellation failed: '.$e->getMessage()); - ray($e->getMessage()); return redirect()->route('dashboard'); } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index e29c4a307..5f1731071 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -67,5 +67,7 @@ class Kernel extends HttpKernel 'signed' => \App\Http\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + 'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class, + 'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class, ]; } diff --git a/app/Http/Middleware/ApiAllowed.php b/app/Http/Middleware/ApiAllowed.php new file mode 100644 index 000000000..dc6be5da3 --- /dev/null +++ b/app/Http/Middleware/ApiAllowed.php @@ -0,0 +1,32 @@ +is_api_enabled === false) { + return response()->json(['success' => true, 'message' => 'API is disabled.'], 403); + } + + if (! isDev()) { + if ($settings->allowed_ips) { + $allowedIps = explode(',', $settings->allowed_ips); + if (! in_array($request->ip(), $allowedIps)) { + return response()->json(['success' => true, 'message' => 'You are not allowed to access the API.'], 403); + } + } + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/IgnoreReadOnlyApiToken.php b/app/Http/Middleware/IgnoreReadOnlyApiToken.php new file mode 100644 index 000000000..bd6cd1f8a --- /dev/null +++ b/app/Http/Middleware/IgnoreReadOnlyApiToken.php @@ -0,0 +1,28 @@ +user()->currentAccessToken(); + if ($token->can('*')) { + return $next($request); + } + if ($token->can('read-only')) { + return response()->json(['message' => 'You are not allowed to perform this action.'], 403); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/OnlyRootApiToken.php b/app/Http/Middleware/OnlyRootApiToken.php new file mode 100644 index 000000000..8ff1fa0e5 --- /dev/null +++ b/app/Http/Middleware/OnlyRootApiToken.php @@ -0,0 +1,25 @@ +user()->currentAccessToken(); + if ($token->can('*')) { + return $next($request); + } + + return response()->json(['message' => 'You are not allowed to perform this action.'], 403); + } +} diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 72d8c0ad1..27f77f7a1 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -9,6 +9,7 @@ use App\Events\ApplicationStatusChanged; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationPreview; +use App\Models\EnvironmentVariable; use App\Models\GithubApp; use App\Models\GitlabApp; use App\Models\Server; @@ -25,6 +26,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Process; use Illuminate\Support\Sleep; use Illuminate\Support\Str; use RuntimeException; @@ -108,10 +110,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private bool $is_debug_enabled; - private $build_args; + private Collection|string $build_args; private $env_args; + private $environment_variables; + private $env_nixpacks_args; private $docker_compose; @@ -126,7 +130,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private string $dockerfile_location = '/Dockerfile'; - private string $docker_compose_location = '/docker-compose.yml'; + private string $docker_compose_location = '/docker-compose.yaml'; private ?string $docker_compose_custom_start_command = null; @@ -156,6 +160,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private ?string $coolify_variables = null; + private bool $preserveRepository = false; + public $tries = 1; public function __construct(int $application_deployment_queue_id) @@ -163,6 +169,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id); $this->application = Application::find($this->application_deployment_queue->application_id); $this->build_pack = data_get($this->application, 'build_pack'); + $this->build_args = collect([]); $this->application_deployment_queue_id = $application_deployment_queue_id; $this->deployment_uuid = $this->application_deployment_queue->deployment_uuid; @@ -186,6 +193,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->server = $this->mainServer = $this->destination->server; $this->serverUser = $this->server->user; $this->is_this_additional_server = $this->application->additional_servers()->wherePivot('server_id', $this->server->id)->count() > 0; + $this->preserveRepository = $this->application->settings->is_preserve_repository_enabled; $this->basedir = $this->application->generateBaseDir($this->deployment_uuid); $this->workdir = "{$this->basedir}".rtrim($this->application->base_directory, '/'); @@ -193,9 +201,14 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->is_debug_enabled = $this->application->settings->is_debug_enabled; $this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id); - ray('New container name: ', $this->container_name); + if ($this->application->settings->custom_internal_name && ! $this->application->settings->is_consistent_container_name_enabled) { + if ($this->pull_request_id === 0) { + $this->container_name = $this->application->settings->custom_internal_name; + } else { + $this->container_name = "{$this->application->settings->custom_internal_name}-pr-{$this->pull_request_id}"; + } + } - savePrivateKeyToFs($this->server); $this->saved_outputs = collect(); // Set preview fqdn @@ -212,12 +225,17 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } } + public function tags(): array + { + return ['server:'.gethostname()]; + } + public function handle(): void { $this->application_deployment_queue->update([ 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, ]); - if (! $this->server->isFunctional()) { + if ($this->server->isFunctional() === false) { $this->application_deployment_queue->addLogEntry('Server is not functional.'); $this->fail('Server is not functional.'); @@ -269,6 +287,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->original_server = $this->server; } else { $this->build_server = $buildServers->random(); + $this->application_deployment_queue->build_server_id = $this->build_server->id; $this->application_deployment_queue->addLogEntry("Found a suitable build server ({$this->build_server->name})."); $this->original_server = $this->server; $this->use_build_server = true; @@ -283,7 +302,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue if ($this->pull_request_id !== 0 && $this->application->is_github_based()) { ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::ERROR); } - ray($e); $this->fail($e); throw $e; } finally { @@ -300,14 +318,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue ] ); - // $this->execute_remote_command( - // [ - // "docker image prune -f >/dev/null 2>&1", - // "hidden" => true, - // "ignore_errors" => true, - // ] - // ); - ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id')); } } @@ -339,7 +349,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private function post_deployment() { if ($this->server->isProxyShouldRun()) { - GetContainersStatus::dispatch($this->server); + GetContainersStatus::dispatch($this->server)->onQueue('high'); // dispatch(new ContainerStatusJob($this->server)); } $this->next(ApplicationDeploymentStatus::FINISHED->value); @@ -382,7 +392,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } else { $this->dockerImageTag = $this->application->docker_registry_image_tag; } - ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}.'"); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}."); $this->generate_image_names(); $this->prepare_builder_image(); @@ -415,15 +424,42 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->prepare_builder_image(); $this->check_git_if_build_needed(); $this->clone_repository(); + if ($this->preserveRepository) { + foreach ($this->application->fileStorages as $fileStorage) { + $path = $fileStorage->fs_path; + $saveName = 'file_stat_'.$fileStorage->id; + $realPathInGit = str($path)->replace($this->application->workdir(), $this->workdir)->value(); + // check if the file is a directory or a file inside the repository + $this->execute_remote_command( + [executeInDocker($this->deployment_uuid, "stat -c '%F' {$realPathInGit}"), 'hidden' => true, 'ignore_errors' => true, 'save' => $saveName] + ); + if ($this->saved_outputs->has($saveName)) { + $fileStat = $this->saved_outputs->get($saveName); + if ($fileStat->value() === 'directory' && ! $fileStorage->is_directory) { + $fileStorage->is_directory = true; + $fileStorage->content = null; + $fileStorage->save(); + $fileStorage->deleteStorageOnServer(); + $fileStorage->saveStorageOnServer(); + } elseif ($fileStat->value() === 'regular file' && $fileStorage->is_directory) { + $fileStorage->is_directory = false; + $fileStorage->is_based_on_git = true; + $fileStorage->save(); + $fileStorage->deleteStorageOnServer(); + $fileStorage->saveStorageOnServer(); + } + } + } + } $this->generate_image_names(); $this->cleanup_git(); $this->application->loadComposeFile(isInit: false); if ($this->application->settings->is_raw_compose_deployment_enabled) { - $this->application->parseRawCompose(); + $this->application->oldRawParser(); $yaml = $composeFile = $this->application->docker_compose_raw; $this->save_environment_variables(); } else { - $composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id, preview_id: data_get($this, 'preview.id')); + $composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id')); $this->save_environment_variables(); if (! is_null($this->env_filename)) { $services = collect($composeFile['services']); @@ -440,11 +476,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue return; } - $yaml = Yaml::dump($composeFile->toArray(), 10); + $yaml = Yaml::dump(convertToArray($composeFile), 10); } $this->docker_compose_base64 = base64_encode($yaml); $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"), 'hidden' => true, + executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"), + 'hidden' => true, ]); // Build new container to limit downtime. $this->application_deployment_queue->addLogEntry('Pulling & building required images.'); @@ -458,7 +495,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue if ($this->env_filename) { $command .= " --env-file {$this->workdir}/{$this->env_filename}"; } - $command .= " --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"; + $command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull"; $this->execute_remote_command( [executeInDocker($this->deployment_uuid, $command), 'hidden' => true], ); @@ -474,49 +511,65 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue // TODO } else { $this->execute_remote_command([ - "docker network inspect '{$networkId}' >/dev/null 2>&1 || docker network create --attachable '{$networkId}' >/dev/null || true", 'hidden' => true, 'ignore_errors' => true, + "docker network inspect '{$networkId}' >/dev/null 2>&1 || docker network create --attachable '{$networkId}' >/dev/null || true", + 'hidden' => true, + 'ignore_errors' => true, ], [ - "docker network connect {$networkId} coolify-proxy || true", 'hidden' => true, 'ignore_errors' => true, + "docker network connect {$networkId} coolify-proxy >/dev/null 2>&1 || true", + 'hidden' => true, + 'ignore_errors' => true, ]); } // Start compose file + $server_workdir = $this->application->workdir(); if ($this->application->settings->is_raw_compose_deployment_enabled) { if ($this->docker_compose_custom_start_command) { + $this->write_deployment_configurations(); $this->execute_remote_command( [executeInDocker($this->deployment_uuid, "cd {$this->workdir} && {$this->docker_compose_custom_start_command}"), 'hidden' => true], ); - $this->write_deployment_configurations(); } else { $this->write_deployment_configurations(); - $server_workdir = $this->application->workdir(); + $this->docker_compose_location = '/docker-compose.yaml'; $command = "{$this->coolify_variables} docker compose"; if ($this->env_filename) { - $command .= " --env-file {$this->workdir}/{$this->env_filename}"; + $command .= " --env-file {$server_workdir}/{$this->env_filename}"; } $command .= " --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d"; - $this->execute_remote_command( ['command' => $command, 'hidden' => true], ); } } else { if ($this->docker_compose_custom_start_command) { + $this->write_deployment_configurations(); $this->execute_remote_command( [executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$this->docker_compose_custom_start_command}"), 'hidden' => true], ); - $this->write_deployment_configurations(); } else { $command = "{$this->coolify_variables} docker compose"; - if ($this->env_filename) { - $command .= " --env-file {$this->workdir}/{$this->env_filename}"; + if ($this->preserveRepository) { + if ($this->env_filename) { + $command .= " --env-file {$server_workdir}/{$this->env_filename}"; + } + $command .= " --project-name {$this->application->uuid} --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d"; + $this->write_deployment_configurations(); + + $this->execute_remote_command( + ['command' => $command, 'hidden' => true], + ); + } else { + if ($this->env_filename) { + $command .= " --env-file {$this->workdir}/{$this->env_filename}"; + } + $command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d"; + $this->execute_remote_command( + [executeInDocker($this->deployment_uuid, $command), 'hidden' => true], + ); + $this->write_deployment_configurations(); } - $command .= " --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d"; - $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, $command), 'hidden' => true], - ); - $this->write_deployment_configurations(); } } @@ -601,26 +654,54 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private function write_deployment_configurations() { + if ($this->preserveRepository) { + if ($this->use_build_server) { + $this->server = $this->original_server; + } + if (str($this->configuration_dir)->isNotEmpty()) { + $this->execute_remote_command( + [ + "mkdir -p $this->configuration_dir", + ], + [ + "docker cp {$this->deployment_uuid}:{$this->workdir}/. {$this->configuration_dir}", + ], + ); + } + foreach ($this->application->fileStorages as $fileStorage) { + if (! $fileStorage->is_based_on_git && ! $fileStorage->is_directory) { + $fileStorage->saveStorageOnServer(); + } + } + if ($this->use_build_server) { + $this->server = $this->build_server; + } + } if (isset($this->docker_compose_base64)) { if ($this->use_build_server) { $this->server = $this->original_server; } $readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at); + + $mainDir = $this->configuration_dir; + if ($this->application->settings->is_raw_compose_deployment_enabled) { + $mainDir = $this->application->workdir(); + } if ($this->pull_request_id === 0) { - $composeFileName = "$this->configuration_dir/docker-compose.yml"; + $composeFileName = "$mainDir/docker-compose.yaml"; } else { - $composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml"; - $this->docker_compose_location = "/docker-compose-pr-{$this->pull_request_id}.yml"; + $composeFileName = "$mainDir/docker-compose-pr-{$this->pull_request_id}.yaml"; + $this->docker_compose_location = "/docker-compose-pr-{$this->pull_request_id}.yaml"; } $this->execute_remote_command( [ - "mkdir -p $this->configuration_dir", + "mkdir -p $mainDir", ], [ "echo '{$this->docker_compose_base64}' | base64 -d | tee $composeFileName > /dev/null", ], [ - "echo '{$readme}' > $this->configuration_dir/README.md", + "echo '{$readme}' > $mainDir/README.md", ] ); if ($this->use_build_server) { @@ -633,45 +714,34 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue { $forceFail = true; if (str($this->application->docker_registry_image_name)->isEmpty()) { - ray('empty docker_registry_image_name'); - return; } if ($this->restart_only) { - ray('restart_only'); - return; } if ($this->application->build_pack === 'dockerimage') { - ray('dockerimage'); - return; } if ($this->use_build_server) { - ray('use_build_server'); $forceFail = true; } if ($this->server->isSwarm() && $this->build_pack !== 'dockerimage') { - ray('isSwarm'); $forceFail = true; } if ($this->application->additional_servers->count() > 0) { - ray('additional_servers'); $forceFail = true; } if ($this->is_this_additional_server) { - ray('this is an additional_servers, no pushy pushy'); - return; } - ray('push_to_docker_registry noww: '.$this->production_image_name); try { instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server); $this->application_deployment_queue->addLogEntry('----------------------------------------'); $this->application_deployment_queue->addLogEntry("Pushing image to docker registry ({$this->production_image_name})."); $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true, + executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), + 'hidden' => true, ], ); if ($this->application->docker_registry_image_tag) { @@ -679,10 +749,14 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->application_deployment_queue->addLogEntry("Tagging and pushing image with {$this->application->docker_registry_image_tag} tag."); $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true, + executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), + 'ignore_errors' => true, + 'hidden' => true, ], [ - executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true, + executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), + 'ignore_errors' => true, + 'hidden' => true, ], ); } @@ -691,7 +765,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue if ($forceFail) { throw new RuntimeException($e->getMessage(), 69420); } - ray($e); } } @@ -779,14 +852,20 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private function check_image_locally_or_remotely() { $this->execute_remote_command([ - "docker images -q {$this->production_image_name} 2>/dev/null", 'hidden' => true, 'save' => 'local_image_found', + "docker images -q {$this->production_image_name} 2>/dev/null", + 'hidden' => true, + 'save' => 'local_image_found', ]); if (str($this->saved_outputs->get('local_image_found'))->isEmpty() && $this->application->docker_registry_image_name) { $this->execute_remote_command([ - "docker pull {$this->production_image_name} 2>/dev/null", 'ignore_errors' => true, 'hidden' => true, + "docker pull {$this->production_image_name} 2>/dev/null", + 'ignore_errors' => true, + 'hidden' => true, ]); $this->execute_remote_command([ - "docker images -q {$this->production_image_name} 2>/dev/null", 'hidden' => true, 'save' => 'local_image_found', + "docker images -q {$this->production_image_name} 2>/dev/null", + 'hidden' => true, + 'save' => 'local_image_found', ]); } } @@ -819,14 +898,24 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } if ($this->application->environment_variables_preview->where('key', 'COOLIFY_FQDN')->isEmpty()) { $envs->push("COOLIFY_FQDN={$this->preview->fqdn}"); + $envs->push("COOLIFY_DOMAIN_URL={$this->preview->fqdn}"); } if ($this->application->environment_variables_preview->where('key', 'COOLIFY_URL')->isEmpty()) { $url = str($this->preview->fqdn)->replace('http://', '')->replace('https://', ''); $envs->push("COOLIFY_URL={$url}"); + $envs->push("COOLIFY_DOMAIN_FQDN={$url}"); } - if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) { - $envs->push("COOLIFY_BRANCH={$local_branch}"); + if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') { + if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) { + $envs->push("COOLIFY_BRANCH=\"{$local_branch}\""); + } + if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { + $envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\""); + } } + + add_coolify_default_environment_variables($this->application, $envs, $this->application->environment_variables_preview); + foreach ($sorted_environment_variables_preview as $env) { $real_value = $env->real_value; if ($env->version === '4.0.0-beta.239') { @@ -841,8 +930,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $envs->push($env->key.'='.$real_value); } // Add PORT if not exists, use the first port as default - if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) { - $envs->push("PORT={$ports[0]}"); + if ($this->build_pack !== 'dockercompose') { + if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) { + $envs->push("PORT={$ports[0]}"); + } } // Add HOST if not exists if ($this->application->environment_variables_preview->where('key', 'HOST')->isEmpty()) { @@ -859,15 +950,31 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } } if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) { - $envs->push("COOLIFY_FQDN={$this->application->fqdn}"); + if ((int) $this->application->compose_parsing_version >= 3) { + $envs->push("COOLIFY_URL={$this->application->fqdn}"); + } else { + $envs->push("COOLIFY_FQDN={$this->application->fqdn}"); + } } if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) { $url = str($this->application->fqdn)->replace('http://', '')->replace('https://', ''); - $envs->push("COOLIFY_URL={$url}"); + if ((int) $this->application->compose_parsing_version >= 3) { + $envs->push("COOLIFY_FQDN={$url}"); + } else { + $envs->push("COOLIFY_URL={$url}"); + } } - if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { - $envs->push("COOLIFY_BRANCH={$local_branch}"); + if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') { + if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { + $envs->push("COOLIFY_BRANCH=\"{$local_branch}\""); + } + if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { + $envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\""); + } } + + add_coolify_default_environment_variables($this->application, $envs, $this->application->environment_variables); + foreach ($sorted_environment_variables as $env) { $real_value = $env->real_value; if ($env->version === '4.0.0-beta.239') { @@ -877,21 +984,21 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $real_value = '\''.$real_value.'\''; } else { $real_value = escapeEnvVariables($env->real_value); - ray($real_value); } } $envs->push($env->key.'='.$real_value); } // Add PORT if not exists, use the first port as default - if ($this->application->environment_variables->where('key', 'PORT')->isEmpty()) { - $envs->push("PORT={$ports[0]}"); + if ($this->build_pack !== 'dockercompose') { + if ($this->application->environment_variables->where('key', 'PORT')->isEmpty()) { + $envs->push("PORT={$ports[0]}"); + } } // Add HOST if not exists if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) { $envs->push('HOST=0.0.0.0'); } } - if ($envs->isEmpty()) { $this->env_filename = null; if ($this->use_build_server) { @@ -944,21 +1051,76 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue ); } } + $this->environment_variables = $envs; } - private function framework_based_notification() + private function elixir_finetunes() { - // Laravel old env variables if ($this->pull_request_id === 0) { - $nixpacks_php_fallback_path = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first(); - $nixpacks_php_root_dir = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first(); + $envType = 'environment_variables'; } else { - $nixpacks_php_fallback_path = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first(); - $nixpacks_php_root_dir = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first(); + $envType = 'environment_variables_preview'; } - if ($nixpacks_php_fallback_path?->value === '/index.php' && $nixpacks_php_root_dir?->value === '/app/public' && $this->newVersionIsHealthy === false) { - $this->application_deployment_queue->addLogEntry('There was a change in how Laravel is deployed. Please update your environment variables to match the new deployment method. More details here: https://coolify.io/docs/resources/laravel', 'stderr'); + $mix_env = $this->application->{$envType}->where('key', 'MIX_ENV')->first(); + if ($mix_env) { + if ($mix_env->is_build_time === false) { + $this->application_deployment_queue->addLogEntry('MIX_ENV environment variable is not set as build time.', type: 'error'); + $this->application_deployment_queue->addLogEntry('Please set MIX_ENV environment variable to be build time variable if you facing any issues with the deployment.', type: 'error'); + } + } else { + $this->application_deployment_queue->addLogEntry('MIX_ENV environment variable not found.', type: 'error'); + $this->application_deployment_queue->addLogEntry('Please add MIX_ENV environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error'); } + $secret_key_base = $this->application->{$envType}->where('key', 'SECRET_KEY_BASE')->first(); + if ($secret_key_base) { + if ($secret_key_base->is_build_time === false) { + $this->application_deployment_queue->addLogEntry('SECRET_KEY_BASE environment variable is not set as build time.', type: 'error'); + $this->application_deployment_queue->addLogEntry('Please set SECRET_KEY_BASE environment variable to be build time variable if you facing any issues with the deployment.', type: 'error'); + } + } else { + $this->application_deployment_queue->addLogEntry('SECRET_KEY_BASE environment variable not found.', type: 'error'); + $this->application_deployment_queue->addLogEntry('Please add SECRET_KEY_BASE environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error'); + } + $database_url = $this->application->{$envType}->where('key', 'DATABASE_URL')->first(); + if ($database_url) { + if ($database_url->is_build_time === false) { + $this->application_deployment_queue->addLogEntry('DATABASE_URL environment variable is not set as build time.', type: 'error'); + $this->application_deployment_queue->addLogEntry('Please set DATABASE_URL environment variable to be build time variable if you facing any issues with the deployment.', type: 'error'); + } + } else { + $this->application_deployment_queue->addLogEntry('DATABASE_URL environment variable not found.', type: 'error'); + $this->application_deployment_queue->addLogEntry('Please add DATABASE_URL environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error'); + } + } + + private function laravel_finetunes() + { + if ($this->pull_request_id === 0) { + $envType = 'environment_variables'; + } else { + $envType = 'environment_variables_preview'; + } + $nixpacks_php_fallback_path = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first(); + $nixpacks_php_root_dir = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first(); + + if (! $nixpacks_php_fallback_path) { + $nixpacks_php_fallback_path = new EnvironmentVariable; + $nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH'; + $nixpacks_php_fallback_path->value = '/index.php'; + $nixpacks_php_fallback_path->is_build_time = false; + $nixpacks_php_fallback_path->application_id = $this->application->id; + $nixpacks_php_fallback_path->save(); + } + if (! $nixpacks_php_root_dir) { + $nixpacks_php_root_dir = new EnvironmentVariable; + $nixpacks_php_root_dir->key = 'NIXPACKS_PHP_ROOT_DIR'; + $nixpacks_php_root_dir->value = '/app/public'; + $nixpacks_php_root_dir->is_build_time = false; + $nixpacks_php_root_dir->application_id = $this->application->id; + $nixpacks_php_root_dir->save(); + } + + return [$nixpacks_php_fallback_path, $nixpacks_php_root_dir]; } private function rolling_update() @@ -976,7 +1138,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->write_deployment_configurations(); $this->server = $this->original_server; } - if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled || isset($this->application->settings->custom_internal_name) || $this->pull_request_id !== 0 || str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) { + if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty() || $this->pull_request_id !== 0 || str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) { $this->application_deployment_queue->addLogEntry('----------------------------------------'); if (count($this->application->ports_mappings_array) > 0) { $this->application_deployment_queue->addLogEntry('Application has ports mapped to the host system, rolling update is not supported.'); @@ -984,7 +1146,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue if ((bool) $this->application->settings->is_consistent_container_name_enabled) { $this->application_deployment_queue->addLogEntry('Consistent container name feature enabled, rolling update is not supported.'); } - if (isset($this->application->settings->custom_internal_name)) { + if (str($this->application->settings->custom_internal_name)->isNotEmpty()) { $this->application_deployment_queue->addLogEntry('Custom internal name is set, rolling update is not supported.'); } if ($this->pull_request_id !== 0) { @@ -1005,7 +1167,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->application_deployment_queue->addLogEntry('Rolling update completed.'); } } - $this->framework_based_notification(); } private function health_check() @@ -1059,13 +1220,13 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->application_deployment_queue->addLogEntry("Healthcheck logs: {$health_check_logs} | Return code: {$health_check_return_code}"); } - if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'healthy') { + if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'healthy') { $this->newVersionIsHealthy = true; $this->application->update(['status' => 'running']); $this->application_deployment_queue->addLogEntry('New container is healthy.'); break; } - if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') { + if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') { $this->newVersionIsHealthy = false; $this->query_logs(); break; @@ -1077,7 +1238,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $sleeptime++; } } - if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'starting') { + if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'starting') { $this->query_logs(); } } @@ -1161,7 +1322,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private function prepare_builder_image() { + $settings = instanceSettings(); $helperImage = config('coolify.helper_image'); + $helperImage = "{$helperImage}:{$settings->helper_version}"; // Get user home directory $this->serverUserHomeDir = instant_remote_process(['echo $HOME'], $this->server); $this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server); @@ -1212,8 +1375,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue return; } if ($destination_ids->contains($this->destination->id)) { - ray('Same destination found in additional destinations. Skipping.'); - return; } foreach ($destination_ids as $destination_id) { @@ -1225,7 +1386,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue continue; } // ray('Deploying to additional destination: ', $server->name); - $deployment_uuid = new Cuid2(); + $deployment_uuid = new Cuid2; queue_application_deployment( deployment_uuid: $deployment_uuid, application: $this->application, @@ -1281,10 +1442,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'), ], [ - executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$local_branch}"), + executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$local_branch}"), 'hidden' => true, 'save' => 'git_commit_sha', - ], + ] ); } else { $this->execute_remote_command( @@ -1313,7 +1474,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } $this->execute_remote_command( [ - $importCommands, 'hidden' => true, + $importCommands, + 'hidden' => true, ] ); $this->create_workdir(); @@ -1366,17 +1528,20 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue throw new RuntimeException('Nixpacks failed to detect the application type. Please check the documentation of Nixpacks: https://nixpacks.com/docs/providers'); } } + if ($this->saved_outputs->get('nixpacks_plan')) { $this->nixpacks_plan = $this->saved_outputs->get('nixpacks_plan'); if ($this->nixpacks_plan) { $this->application_deployment_queue->addLogEntry("Found application type: {$this->nixpacks_type}."); $this->application_deployment_queue->addLogEntry("If you need further customization, please check the documentation of Nixpacks: https://nixpacks.com/docs/providers/{$this->nixpacks_type}"); $parsed = Toml::Parse($this->nixpacks_plan); + // Do any modifications here $this->generate_env_variables(); $merged_envs = $this->env_args->merge(collect(data_get($parsed, 'variables', []))); $aptPkgs = data_get($parsed, 'phases.setup.aptPkgs', []); if (count($aptPkgs) === 0) { + $aptPkgs = ['curl', 'wget']; data_set($parsed, 'phases.setup.aptPkgs', ['curl', 'wget']); } else { if (! in_array('curl', $aptPkgs)) { @@ -1388,8 +1553,22 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue data_set($parsed, 'phases.setup.aptPkgs', $aptPkgs); } data_set($parsed, 'variables', $merged_envs->toArray()); + $is_laravel = data_get($parsed, 'variables.IS_LARAVEL', false); + if ($is_laravel) { + $variables = $this->laravel_finetunes(); + data_set($parsed, 'variables.NIXPACKS_PHP_FALLBACK_PATH', $variables[0]->value); + data_set($parsed, 'variables.NIXPACKS_PHP_ROOT_DIR', $variables[1]->value); + } + if ($this->nixpacks_type === 'elixir') { + $this->elixir_finetunes(); + } $this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT); $this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true); + if ($this->nixpacks_type === 'rust') { + // temporary: disable healthcheck for rust because the start phase does not have curl/wget + $this->application->health_check_enabled = false; + $this->application->save(); + } } } } @@ -1492,7 +1671,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->application->custom_labels = base64_encode($labels->implode("\n")); $this->application->save(); } else { - $labels = collect(generateLabelsApplication($this->application, $this->preview)); + if (! $this->application->settings->is_container_label_readonly_enabled) { + $labels = collect(generateLabelsApplication($this->application, $this->preview)); + } } if ($this->pull_request_id !== 0) { $labels = collect(generateLabelsApplication($this->application, $this->preview)); @@ -1507,9 +1688,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue // Check for custom HEALTHCHECK if ($this->application->build_pack === 'dockerfile' || $this->application->dockerfile) { $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, 'save' => 'dockerfile_from_repo', 'ignore_errors' => true, + executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), + 'hidden' => true, + 'save' => 'dockerfile_from_repo', + 'ignore_errors' => true, ]); - $dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile_from_repo'))->trim()->explode("\n")); + $dockerfile = collect(str($this->saved_outputs->get('dockerfile_from_repo'))->trim()->explode("\n")); $this->application->parseHealthcheckFromDockerfile($dockerfile); } $docker_compose = [ @@ -1542,23 +1726,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue ], ], ]; - if (isset($this->application->settings->custom_internal_name)) { - $docker_compose['services'][$this->container_name]['networks'][$this->destination->network]['aliases'][] = $this->application->settings->custom_internal_name; - } - // if (str($this->saved_outputs->get('dotenv'))->isNotEmpty()) { - // if (data_get($docker_compose, "services.{$this->container_name}.env_file")) { - // $docker_compose['services'][$this->container_name]['env_file'][] = '.env'; - // } else { - // $docker_compose['services'][$this->container_name]['env_file'] = ['.env']; - // } - // } - // if ($this->env_filename) { - // if (data_get($docker_compose, "services.{$this->container_name}.env_file")) { - // $docker_compose['services'][$this->container_name]['env_file'][] = $this->env_filename; - // } else { - // $docker_compose['services'][$this->container_name]['env_file'] = [$this->env_filename]; - // } - // } if (! is_null($this->env_filename)) { $docker_compose['services'][$this->container_name]['env_file'] = [$this->env_filename]; } @@ -1610,12 +1777,15 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue ], ], ]; + if (data_get($this->application, 'swarm_placement_constraints')) { + $swarm_placement_constraints = Yaml::parse(base64_decode(data_get($this->application, 'swarm_placement_constraints'))); + $docker_compose['services'][$this->container_name]['deploy'] = array_merge( + $docker_compose['services'][$this->container_name]['deploy'], + $swarm_placement_constraints + ); + } if (data_get($this->application, 'settings.is_swarm_only_worker_nodes')) { - $docker_compose['services'][$this->container_name]['deploy']['placement'] = [ - 'constraints' => [ - 'node.role == worker', - ], - ]; + $docker_compose['services'][$this->container_name]['deploy']['placement']['constraints'][] = 'node.role == worker'; } if ($this->pull_request_id !== 0) { $docker_compose['services'][$this->container_name]['deploy']['replicas'] = 1; @@ -1624,14 +1794,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $docker_compose['services'][$this->container_name]['labels'] = $labels; } if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) { - $docker_compose['services'][$this->container_name]['logging'] = [ - 'driver' => 'fluentd', - 'options' => [ - 'fluentd-address' => 'tcp://127.0.0.1:24224', - 'fluentd-async' => 'true', - 'fluentd-sub-second-precision' => 'true', - ], - ]; + $docker_compose['services'][$this->container_name]['logging'] = generate_fluentd_configuration(); } if ($this->application->settings->is_gpu_enabled) { $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'] = [ @@ -1658,43 +1821,46 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue if (count($this->application->ports_mappings_array) > 0 && $this->pull_request_id === 0) { $docker_compose['services'][$this->container_name]['ports'] = $this->application->ports_mappings_array; } + if (count($persistent_storages) > 0) { - $docker_compose['services'][$this->container_name]['volumes'] = $persistent_storages; + if (! data_get($docker_compose, 'services.'.$this->container_name.'.volumes')) { + $docker_compose['services'][$this->container_name]['volumes'] = []; + } + $docker_compose['services'][$this->container_name]['volumes'] = array_merge($docker_compose['services'][$this->container_name]['volumes'], $persistent_storages); } if (count($persistent_file_volumes) > 0) { - $docker_compose['services'][$this->container_name]['volumes'] = $persistent_file_volumes->map(function ($item) { + if (! data_get($docker_compose, 'services.'.$this->container_name.'.volumes')) { + $docker_compose['services'][$this->container_name]['volumes'] = []; + } + $docker_compose['services'][$this->container_name]['volumes'] = array_merge($docker_compose['services'][$this->container_name]['volumes'], $persistent_file_volumes->map(function ($item) { return "$item->fs_path:$item->mount_path"; - })->toArray(); + })->toArray()); } if (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } - // if ($this->build_pack === 'dockerfile') { - // $docker_compose['services'][$this->container_name]['build'] = [ - // 'context' => $this->workdir, - // 'dockerfile' => $this->workdir . $this->dockerfile_location, - // ]; - // } if ($this->pull_request_id === 0) { - $custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options); + $custom_compose = convertDockerRunToCompose($this->application->custom_docker_run_options); if ((bool) $this->application->settings->is_consistent_container_name_enabled) { - $docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name]; - if (count($custom_compose) > 0) { - $ipv4 = data_get($custom_compose, 'ip.0'); - $ipv6 = data_get($custom_compose, 'ip6.0'); - data_forget($custom_compose, 'ip'); - data_forget($custom_compose, 'ip6'); - if ($ipv4 || $ipv6) { - data_forget($docker_compose['services'][$this->application->uuid], 'networks'); + if (! $this->application->settings->custom_internal_name) { + $docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name]; + if (count($custom_compose) > 0) { + $ipv4 = data_get($custom_compose, 'ip.0'); + $ipv6 = data_get($custom_compose, 'ip6.0'); + data_forget($custom_compose, 'ip'); + data_forget($custom_compose, 'ip6'); + if ($ipv4 || $ipv6) { + data_forget($docker_compose['services'][$this->application->uuid], 'networks'); + } + if ($ipv4) { + $docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv4_address'] = $ipv4; + } + if ($ipv6) { + $docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv6_address'] = $ipv6; + } + $docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose); } - if ($ipv4) { - $docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv4_address'] = $ipv4; - } - if ($ipv6) { - $docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv6_address'] = $ipv6; - } - $docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose); } } else { if (count($custom_compose) > 0) { @@ -1718,7 +1884,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->docker_compose = Yaml::dump($docker_compose, 10); $this->docker_compose_base64 = base64_encode($this->docker_compose); - $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}/docker-compose.yml > /dev/null"), 'hidden' => true]); + $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}/docker-compose.yaml > /dev/null"), 'hidden' => true]); } private function generate_local_persistent_volumes() @@ -1791,13 +1957,23 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->application_deployment_queue->addLogEntry("Pulling latest image ($image) from the registry."); $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "docker pull {$image}"), 'hidden' => true, + executeInDocker($this->deployment_uuid, "docker pull {$image}"), + 'hidden' => true, ] ); } private function build_image() { + // Add Coolify related variables to the build args + $this->environment_variables->filter(function ($key, $value) { + return str($key)->startsWith('COOLIFY_'); + })->each(function ($key, $value) { + $this->build_args->push("--build-arg '{$key}'"); + }); + + $this->build_args = $this->build_args->implode(' '); + $this->application_deployment_queue->addLogEntry('----------------------------------------'); if ($this->application->build_pack === 'static') { $this->application_deployment_queue->addLogEntry('Static deployment. Copying static assets to the image.'); @@ -1819,35 +1995,44 @@ COPY . . RUN rm -f /usr/share/nginx/html/nginx.conf RUN rm -f /usr/share/nginx/html/Dockerfile COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); - $nginx_config = base64_encode('server { - listen 80; - listen [::]:80; - server_name localhost; - - location / { - root /usr/share/nginx/html; - index index.html; - try_files $uri $uri.html $uri/index.html $uri/ /index.html =404; + if (str($this->application->custom_nginx_configuration)->isNotEmpty()) { + $nginx_config = base64_encode($this->application->custom_nginx_configuration); + } else { + $nginx_config = base64_encode(defaultNginxConfiguration()); } - - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } - }'); } else { if ($this->application->build_pack === 'nixpacks') { $this->nixpacks_plan = base64_encode($this->nixpacks_plan); $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]); if ($this->force_rebuild) { $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir}"), 'hidden' => true, + executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), + 'hidden' => true, ]); + $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}"; } else { $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir}"), 'hidden' => true, + executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), + 'hidden' => true, ]); + $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}"; } + + $base64_build_command = base64_encode($build_command); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), + 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), + 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), + 'hidden' => true, + ] + ); $this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]); } else { if ($this->force_rebuild) { @@ -1859,36 +2044,29 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); } $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), + 'hidden' => true, ], [ - executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true, + executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), + 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), + 'hidden' => true, ] ); } - $dockerfile = base64_encode("FROM {$this->application->static_image} WORKDIR /usr/share/nginx/html/ LABEL coolify.deploymentId={$this->deployment_uuid} COPY --from=$this->build_image_name /app/{$this->application->publish_directory} . COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); - - $nginx_config = base64_encode('server { - listen 80; - listen [::]:80; - server_name localhost; - - location / { - root /usr/share/nginx/html; - index index.html; - try_files $uri $uri.html $uri/index.html $uri/ /index.html =404; + if (str($this->application->custom_nginx_configuration)->isNotEmpty()) { + $nginx_config = base64_encode($this->application->custom_nginx_configuration); + } else { + $nginx_config = base64_encode(defaultNginxConfiguration()); } - - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } - }'); } $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; $base64_build_command = base64_encode($build_command); @@ -1900,10 +2078,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d | tee {$this->workdir}/nginx.conf > /dev/null"), ], [ - executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), + 'hidden' => true, ], [ - executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true, + executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), + 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), + 'hidden' => true, ] ); } else { @@ -1917,10 +2101,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); $base64_build_command = base64_encode($build_command); $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), + 'hidden' => true, ], [ - executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true, + executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), + 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), + 'hidden' => true, ] ); } else { @@ -1929,13 +2119,32 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]); if ($this->force_rebuild) { $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir}"), 'hidden' => true, + executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), + 'hidden' => true, ]); + $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; } else { $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir}"), 'hidden' => true, + executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), + 'hidden' => true, ]); + $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; } + $base64_build_command = base64_encode($build_command); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), + 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), + 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), + 'hidden' => true, + ] + ); $this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]); } else { if ($this->force_rebuild) { @@ -1947,10 +2156,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); } $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), + 'hidden' => true, ], [ - executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true, + executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), + 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), + 'hidden' => true, ] ); } @@ -1959,27 +2174,62 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); $this->application_deployment_queue->addLogEntry('Building docker image completed.'); } + private function graceful_shutdown_container(string $containerName, int $timeout = 300) + { + try { + $process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); + + $startTime = time(); + while ($process->running()) { + if (time() - $startTime >= $timeout) { + $this->execute_remote_command( + ["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true] + ); + break; + } + usleep(100000); + } + + $isRunning = $this->execute_remote_command( + ["docker inspect -f '{{.State.Running}}' $containerName", 'hidden' => true, 'ignore_errors' => true] + ) === 'true'; + + if ($isRunning) { + $this->execute_remote_command( + ["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true] + ); + } + } catch (\Exception $error) { + $this->application_deployment_queue->addLogEntry("Error stopping container $containerName: ".$error->getMessage(), 'stderr'); + } + + $this->remove_container($containerName); + } + + private function remove_container(string $containerName) + { + $this->execute_remote_command( + ["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true] + ); + } + private function stop_running_container(bool $force = false) { $this->application_deployment_queue->addLogEntry('Removing old containers.'); if ($this->newVersionIsHealthy || $force) { - $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); - if ($this->pull_request_id === 0) { - $containers = $containers->filter(function ($container) { - return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name.'-pr-'.$this->pull_request_id; + if ($this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty()) { + $this->graceful_shutdown_container($this->container_name); + } else { + $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); + if ($this->pull_request_id === 0) { + $containers = $containers->filter(function ($container) { + return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name.'-pr-'.$this->pull_request_id; + }); + } + $containers->each(function ($container) { + $this->graceful_shutdown_container(data_get($container, 'Names')); }); } - $containers->each(function ($container) { - $containerName = data_get($container, 'Names'); - $this->execute_remote_command( - ["docker rm -f $containerName >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true], - ); - }); - if ($this->application->settings->is_consistent_container_name_enabled || isset($this->application->settings->custom_internal_name)) { - $this->execute_remote_command( - ["docker rm -f $this->container_name >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true], - ); - } } else { if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile' || $this->application->build_pack === 'dockerimage') { $this->application_deployment_queue->addLogEntry('----------------------------------------'); @@ -1990,45 +2240,26 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); $this->application_deployment_queue->update([ 'status' => ApplicationDeploymentStatus::FAILED->value, ]); - $this->execute_remote_command( - ["docker rm -f $this->container_name >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true], - ); + $this->graceful_shutdown_container($this->container_name); } } - private function build_by_compose_file() - { - $this->application_deployment_queue->addLogEntry('Pulling & building required images.'); - if ($this->application->build_pack === 'dockerimage') { - $this->application_deployment_queue->addLogEntry('Pulling latest images from the registry.'); - $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), 'hidden' => true], - [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} build"), 'hidden' => true], - ); - } else { - $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), 'hidden' => true], - ); - } - $this->application_deployment_queue->addLogEntry('New images built.'); - } - private function start_by_compose_file() { if ($this->application->build_pack === 'dockerimage') { $this->application_deployment_queue->addLogEntry('Pulling latest images from the registry.'); $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), 'hidden' => true], - [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} up --build -d"), 'hidden' => true], + [executeInDocker($this->deployment_uuid, "docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} pull"), 'hidden' => true], + [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} up --build -d"), 'hidden' => true], ); } else { if ($this->use_build_server) { $this->execute_remote_command( - ["{$this->coolify_variables} docker compose --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", 'hidden' => true], + ["{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", 'hidden' => true], ); } else { $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), 'hidden' => true], + [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), 'hidden' => true], ); } } @@ -2049,17 +2280,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); $this->build_args->push("--build-arg {$env->key}={$value}"); } } - - $this->build_args = $this->build_args->implode(' '); - ray($this->build_args); } private function add_build_env_variables_to_dockerfile() { $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, 'save' => 'dockerfile', + executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), + 'hidden' => true, + 'save' => 'dockerfile', ]); - $dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); + $dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); if ($this->pull_request_id === 0) { foreach ($this->application->build_environment_variables as $env) { if (data_get($env, 'is_multiline') === true) { @@ -2075,7 +2305,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); } else { $dockerfile->splice(1, 0, "ARG {$env->key}={$env->real_value}"); } - $dockerfile->splice(1, 0, "ARG {$env->key}={$env->real_value}"); } } $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); @@ -2103,7 +2332,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); $exec = "docker exec {$containerName} {$cmd}"; $this->execute_remote_command( [ - 'command' => $exec, 'hidden' => true, + 'command' => $exec, + 'hidden' => true, ], ); @@ -2130,7 +2360,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); try { $this->execute_remote_command( [ - 'command' => $exec, 'hidden' => true, 'save' => 'post-deployment-command-output', + 'command' => $exec, + 'hidden' => true, + 'save' => 'post-deployment-command-output', ], ); } catch (Exception $e) { @@ -2181,13 +2413,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); if ($this->application->build_pack !== 'dockercompose') { $code = $exception->getCode(); - ray($code); if ($code !== 69420) { // 69420 means failed to push the image to the registry, so we don't need to remove the new version as it is the currently running one - $this->application_deployment_queue->addLogEntry('Deployment failed. Removing the new version of your application.', 'stderr'); - $this->execute_remote_command( - ["docker rm -f $this->container_name >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true] - ); + if ($this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty()) { + // do not remove already running container + } else { + $this->application_deployment_queue->addLogEntry('Deployment failed. Removing the new version of your application.', 'stderr'); + $this->execute_remote_command( + ["docker rm -f $this->container_name >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true] + ); + } } } } diff --git a/app/Jobs/ApplicationPullRequestUpdateJob.php b/app/Jobs/ApplicationPullRequestUpdateJob.php index d400642dd..2eefc4dd2 100755 --- a/app/Jobs/ApplicationPullRequestUpdateJob.php +++ b/app/Jobs/ApplicationPullRequestUpdateJob.php @@ -25,15 +25,12 @@ class ApplicationPullRequestUpdateJob implements ShouldBeEncrypted, ShouldQueue public ApplicationPreview $preview, public ProcessStatus $status, public ?string $deployment_uuid = null - ) { - } + ) {} public function handle() { try { if ($this->application->is_public_repository()) { - ray('Public repository. Skipping comment update.'); - return; } if ($this->status === ProcessStatus::CLOSED) { @@ -54,16 +51,12 @@ class ApplicationPullRequestUpdateJob implements ShouldBeEncrypted, ShouldQueue $this->body .= '[Open Build Logs]('.$this->build_logs_url.")\n\n\n"; $this->body .= 'Last updated at: '.now()->toDateTimeString().' CET'; - - ray('Updating comment', $this->body); if ($this->preview->pull_request_issue_comment_id) { $this->update_comment(); } else { $this->create_comment(); } } catch (\Throwable $e) { - ray($e); - return $e; } } @@ -74,7 +67,6 @@ class ApplicationPullRequestUpdateJob implements ShouldBeEncrypted, ShouldQueue 'body' => $this->body, ], throwError: false); if (data_get($data, 'message') === 'Not Found') { - ray('Comment not found. Creating new one.'); $this->create_comment(); } } diff --git a/app/Jobs/ApplicationRestartJob.php b/app/Jobs/ApplicationRestartJob.php deleted file mode 100644 index 54c062197..000000000 --- a/app/Jobs/ApplicationRestartJob.php +++ /dev/null @@ -1,32 +0,0 @@ -applicationDeploymentQueueId = $applicationDeploymentQueueId; - } - - public function handle() - { - ray('Restarting application'); - } -} diff --git a/app/Jobs/CheckAndStartSentinelJob.php b/app/Jobs/CheckAndStartSentinelJob.php new file mode 100644 index 000000000..788db89ea --- /dev/null +++ b/app/Jobs/CheckAndStartSentinelJob.php @@ -0,0 +1,52 @@ +server, false); + $sentinelFoundJson = json_decode($sentinelFound, true); + $sentinelStatus = data_get($sentinelFoundJson, '0.State.Status', 'exited'); + if ($sentinelStatus !== 'running') { + StartSentinel::run(server: $this->server, restart: true, latestVersion: $latestVersion); + + return; + } + // If sentinel is running, check if it needs an update + $runningVersion = instant_remote_process(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/version"'], $this->server, false); + if (empty($runningVersion)) { + $runningVersion = '0.0.0'; + } + if ($latestVersion === '0.0.0' && $runningVersion === '0.0.0') { + StartSentinel::run(server: $this->server, restart: true, latestVersion: 'latest'); + + return; + } else { + if (version_compare($runningVersion, $latestVersion, '<')) { + StartSentinel::run(server: $this->server, restart: true, latestVersion: $latestVersion); + + return; + } + } + } +} diff --git a/app/Jobs/CheckForUpdatesJob.php b/app/Jobs/CheckForUpdatesJob.php new file mode 100644 index 000000000..f2348118a --- /dev/null +++ b/app/Jobs/CheckForUpdatesJob.php @@ -0,0 +1,44 @@ +get('https://cdn.coollabs.io/coolify/versions.json'); + if ($response->successful()) { + $versions = $response->json(); + + $latest_version = data_get($versions, 'coolify.v4.version'); + $current_version = config('version'); + + if (version_compare($latest_version, $current_version, '>')) { + // New version available + $settings->update(['new_version_available' => true]); + File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT)); + } else { + $settings->update(['new_version_available' => false]); + } + } + } catch (\Throwable $e) { + // Consider implementing a notification to administrators + } + } +} diff --git a/app/Jobs/CheckHelperImageJob.php b/app/Jobs/CheckHelperImageJob.php new file mode 100644 index 000000000..6abb8a150 --- /dev/null +++ b/app/Jobs/CheckHelperImageJob.php @@ -0,0 +1,39 @@ +get('https://cdn.coollabs.io/coolify/versions.json'); + if ($response->successful()) { + $versions = $response->json(); + $settings = instanceSettings(); + $latest_version = data_get($versions, 'coolify.helper.version'); + $current_version = $settings->helper_version; + if (version_compare($latest_version, $current_version, '>')) { + $settings->update(['helper_version' => $latest_version]); + } + } + } catch (\Throwable $e) { + send_internal_notification('CheckHelperImageJob failed with: '.$e->getMessage()); + throw $e; + } + } +} diff --git a/app/Jobs/CheckLogDrainContainerJob.php b/app/Jobs/CheckLogDrainContainerJob.php deleted file mode 100644 index 312200f66..000000000 --- a/app/Jobs/CheckLogDrainContainerJob.php +++ /dev/null @@ -1,95 +0,0 @@ -server->id))->dontRelease()]; - } - - public function uniqueId(): int - { - return $this->server->id; - } - - public function healthcheck() - { - $status = instant_remote_process(["docker inspect --format='{{json .State.Status}}' coolify-log-drain"], $this->server, false); - if (str($status)->contains('running')) { - return true; - } else { - return false; - } - } - - public function handle() - { - // ray("checking log drain statuses for {$this->server->id}"); - try { - if (! $this->server->isFunctional()) { - return; - } - $containers = instant_remote_process(['docker container ls -q'], $this->server, false); - if (! $containers) { - return; - } - $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server); - $containers = format_docker_command_output_to_json($containers); - - $foundLogDrainContainer = $containers->filter(function ($value, $key) { - return data_get($value, 'Name') === '/coolify-log-drain'; - })->first(); - if (! $foundLogDrainContainer || ! $this->healthcheck()) { - ray('Log drain container not found or unhealthy. Restarting...'); - InstallLogDrain::run($this->server); - Sleep::for(10)->seconds(); - if ($this->healthcheck()) { - if ($this->server->log_drain_notification_sent) { - $this->server->team?->notify(new ContainerRestarted('Coolify Log Drainer', $this->server)); - $this->server->update(['log_drain_notification_sent' => false]); - } - - return; - } - if (! $this->server->log_drain_notification_sent) { - ray('Log drain container still unhealthy. Sending notification...'); - // $this->server->team?->notify(new ContainerStopped('Coolify Log Drainer', $this->server, null)); - $this->server->update(['log_drain_notification_sent' => true]); - } - } else { - if ($this->server->log_drain_notification_sent) { - $this->server->team?->notify(new ContainerRestarted('Coolify Log Drainer', $this->server)); - $this->server->update(['log_drain_notification_sent' => false]); - } - } - } catch (\Throwable $e) { - if (! isCloud()) { - send_internal_notification("CheckLogDrainContainerJob failed on ({$this->server->id}) with: ".$e->getMessage()); - } - ray($e->getMessage()); - - return handleError($e); - } - } -} diff --git a/app/Jobs/CheckResaleLicenseJob.php b/app/Jobs/CheckResaleLicenseJob.php index 8f2039ef2..7479867b6 100644 --- a/app/Jobs/CheckResaleLicenseJob.php +++ b/app/Jobs/CheckResaleLicenseJob.php @@ -14,9 +14,7 @@ class CheckResaleLicenseJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct() - { - } + public function __construct() {} public function handle(): void { @@ -24,7 +22,6 @@ class CheckResaleLicenseJob implements ShouldBeEncrypted, ShouldQueue CheckResaleLicense::run(); } catch (\Throwable $e) { send_internal_notification('CheckResaleLicenseJob failed with: '.$e->getMessage()); - ray($e); throw $e; } } diff --git a/app/Jobs/CleanupHelperContainersJob.php b/app/Jobs/CleanupHelperContainersJob.php index 418c7a0f4..f185ab781 100644 --- a/app/Jobs/CleanupHelperContainersJob.php +++ b/app/Jobs/CleanupHelperContainersJob.php @@ -15,26 +15,20 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(public Server $server) - { - } + public function __construct(public Server $server) {} public function handle(): void { try { - ray('Cleaning up helper containers on '.$this->server->name); - $containers = instant_remote_process(['docker container ps --filter "ancestor=ghcr.io/coollabsio/coolify-helper:next" --filter "ancestor=ghcr.io/coollabsio/coolify-helper:latest" --format \'{{json .}}\''], $this->server, false); - $containers = format_docker_command_output_to_json($containers); - if ($containers->count() > 0) { - foreach ($containers as $container) { - $containerId = data_get($container, 'ID'); - ray('Removing container '.$containerId); + $containers = instant_remote_process(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false); + $containerIds = collect(json_decode($containers))->pluck('ID'); + if ($containerIds->count() > 0) { + foreach ($containerIds as $containerId) { instant_remote_process(['docker container rm -f '.$containerId], $this->server, false); } } } catch (\Throwable $e) { send_internal_notification('CleanupHelperContainersJob failed with error: '.$e->getMessage()); - ray($e->getMessage()); } } } diff --git a/app/Jobs/CleanupInstanceStuffsJob.php b/app/Jobs/CleanupInstanceStuffsJob.php index b846ad2bc..84f14ed02 100644 --- a/app/Jobs/CleanupInstanceStuffsJob.php +++ b/app/Jobs/CleanupInstanceStuffsJob.php @@ -3,54 +3,37 @@ namespace App\Jobs; use App\Models\TeamInvitation; -use App\Models\Waitlist; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Log; class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct() + public function __construct() {} + + public function middleware(): array { - + return [(new WithoutOverlapping('cleanup-instance-stuffs'))->dontRelease()]; } - // public function uniqueId(): string - // { - // return $this->container_name; - // } - public function handle(): void { try { - // $this->cleanup_waitlist(); + $this->cleanupInvitationLink(); } catch (\Throwable $e) { - send_internal_notification('CleanupInstanceStuffsJob failed with error: '.$e->getMessage()); - ray($e->getMessage()); - } - try { - $this->cleanup_invitation_link(); - } catch (\Throwable $e) { - send_internal_notification('CleanupInstanceStuffsJob failed with error: '.$e->getMessage()); - ray($e->getMessage()); + Log::error('CleanupInstanceStuffsJob failed with error: '.$e->getMessage()); } } - private function cleanup_waitlist() - { - $waitlist = Waitlist::whereVerified(false)->where('created_at', '<', now()->subMinutes(config('constants.waitlist.expiration')))->get(); - foreach ($waitlist as $item) { - $item->delete(); - } - } - - private function cleanup_invitation_link() + private function cleanupInvitationLink() { $invitation = TeamInvitation::all(); foreach ($invitation as $item) { diff --git a/app/Jobs/CleanupStaleMultiplexedConnections.php b/app/Jobs/CleanupStaleMultiplexedConnections.php new file mode 100644 index 000000000..6d49bee4b --- /dev/null +++ b/app/Jobs/CleanupStaleMultiplexedConnections.php @@ -0,0 +1,82 @@ +cleanupStaleConnections(); + $this->cleanupNonExistentServerConnections(); + } + + private function cleanupStaleConnections() + { + $muxFiles = Storage::disk('ssh-mux')->files(); + + foreach ($muxFiles as $muxFile) { + $serverUuid = $this->extractServerUuidFromMuxFile($muxFile); + $server = Server::where('uuid', $serverUuid)->first(); + + if (! $server) { + $this->removeMultiplexFile($muxFile); + + continue; + } + + $muxSocket = "/var/www/html/storage/app/ssh/mux/{$muxFile}"; + $checkCommand = "ssh -O check -o ControlPath={$muxSocket} {$server->user}@{$server->ip} 2>/dev/null"; + $checkProcess = Process::run($checkCommand); + + if ($checkProcess->exitCode() !== 0) { + $this->removeMultiplexFile($muxFile); + } else { + $muxContent = Storage::disk('ssh-mux')->get($muxFile); + $establishedAt = Carbon::parse(substr($muxContent, 37)); + $expirationTime = $establishedAt->addSeconds(config('constants.ssh.mux_persist_time')); + + if (Carbon::now()->isAfter($expirationTime)) { + $this->removeMultiplexFile($muxFile); + } + } + } + } + + private function cleanupNonExistentServerConnections() + { + $muxFiles = Storage::disk('ssh-mux')->files(); + $existingServerUuids = Server::pluck('uuid')->toArray(); + + foreach ($muxFiles as $muxFile) { + $serverUuid = $this->extractServerUuidFromMuxFile($muxFile); + if (! in_array($serverUuid, $existingServerUuids)) { + $this->removeMultiplexFile($muxFile); + } + } + } + + private function extractServerUuidFromMuxFile($muxFile) + { + return substr($muxFile, 4); + } + + private function removeMultiplexFile($muxFile) + { + $muxSocket = "/var/www/html/storage/app/ssh/mux/{$muxFile}"; + $closeCommand = "ssh -O exit -o ControlPath={$muxSocket} localhost 2>/dev/null"; + Process::run($closeCommand); + Storage::disk('ssh-mux')->delete($muxFile); + } +} diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index c50d17d4c..22ae06ebd 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue @@ -23,19 +22,7 @@ class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue return isDev() ? 1 : 3; } - public function __construct(public Server $server) - { - } - - public function middleware(): array - { - return [(new WithoutOverlapping($this->server->uuid))]; - } - - public function uniqueId(): int - { - return $this->server->uuid; - } + public function __construct(public Server $server) {} public function handle() { diff --git a/app/Jobs/CoolifyTask.php b/app/Jobs/CoolifyTask.php index e5f4dfd5e..c3692c30b 100755 --- a/app/Jobs/CoolifyTask.php +++ b/app/Jobs/CoolifyTask.php @@ -20,11 +20,10 @@ class CoolifyTask implements ShouldBeEncrypted, ShouldQueue */ public function __construct( public Activity $activity, - public bool $ignore_errors = false, - public $call_event_on_finish = null, - public $call_event_data = null - ) { - } + public bool $ignore_errors, + public $call_event_on_finish, + public $call_event_data, + ) {} /** * Execute the job. diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 07386988c..fcfe2fe3d 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -2,7 +2,6 @@ namespace App\Jobs; -use App\Actions\Database\StopDatabase; use App\Events\BackupCreated; use App\Models\S3Storage; use App\Models\ScheduledDatabaseBackup; @@ -22,7 +21,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Str; @@ -56,55 +54,47 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue public ?string $backup_output = null; + public ?string $postgres_password = null; + public ?S3Storage $s3 = null; public function __construct($backup) { $this->backup = $backup; - $this->team = Team::find($backup->team_id); - if (is_null($this->team)) { - return; - } - if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') { - $this->database = data_get($this->backup, 'database'); - $this->server = $this->database->service->server; - $this->s3 = $this->backup->s3; - } else { - $this->database = data_get($this->backup, 'database'); - $this->server = $this->database->destination->server; - $this->s3 = $this->backup->s3; - } - } - - public function middleware(): array - { - return [new WithoutOverlapping($this->backup->id)]; - } - - public function uniqueId(): int - { - return $this->backup->id; } public function handle(): void { try { + $this->team = Team::find($this->backup->team_id); + if (! $this->team) { + $this->backup->delete(); + + return; + } + if (data_get($this->backup, 'database_type') === \App\Models\ServiceDatabase::class) { + $this->database = data_get($this->backup, 'database'); + $this->server = $this->database->service->server; + $this->s3 = $this->backup->s3; + } else { + $this->database = data_get($this->backup, 'database'); + $this->server = $this->database->destination->server; + $this->s3 = $this->backup->s3; + } + if (is_null($this->server)) { + throw new \Exception('Server not found?!'); + } + if (is_null($this->database)) { + throw new \Exception('Database not found?!'); + } + BackupCreated::dispatch($this->team->id); - // Check if team is exists - if (is_null($this->team)) { - $this->backup->update(['status' => 'failed']); - StopDatabase::run($this->database); - $this->database->delete(); - return; - } - $status = Str::of(data_get($this->database, 'status')); + $status = str(data_get($this->database, 'status')); if (! $status->startsWith('running') && $this->database->id !== 0) { - ray('database not running'); - return; } - if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') { + if (data_get($this->backup, 'database_type') === \App\Models\ServiceDatabase::class) { $databaseType = $this->database->databaseType(); $serviceUuid = $this->database->service->uuid; $serviceName = str($this->database->service->name)->slug(); @@ -133,6 +123,12 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue } else { $databasesToBackup = $this->database->postgres_user; } + $this->postgres_password = $envs->filter(function ($env) { + return str($env)->startsWith('POSTGRES_PASSWORD='); + })->first(); + if ($this->postgres_password) { + $this->postgres_password = str($this->postgres_password)->after('POSTGRES_PASSWORD=')->value(); + } } elseif (str($databaseType)->contains('mysql')) { $this->container_name = "{$this->database->name}-$serviceUuid"; $this->directory_name = $serviceName.'-'.$this->container_name; @@ -223,7 +219,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue // Format: db1:collection1,collection2|db2:collection3,collection4 $databasesToBackup = explode('|', $databasesToBackup); $databasesToBackup = array_map('trim', $databasesToBackup); - ray($databasesToBackup); } elseif (str($databaseType)->contains('mysql')) { // Format: db1,db2,db3 $databasesToBackup = explode(',', $databasesToBackup); @@ -236,8 +231,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue return; } } - $this->backup_dir = backup_dir().'/databases/'.Str::of($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name; - + $this->backup_dir = backup_dir().'/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name; if ($this->database->name === 'coolify-db') { $databasesToBackup = ['coolify']; $this->directory_name = $this->container_name = 'coolify-db'; @@ -246,10 +240,12 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue } foreach ($databasesToBackup as $database) { $size = 0; - ray('Backing up '.$database); try { if (str($databaseType)->contains('postgres')) { $this->backup_file = "/pg-dump-$database-".Carbon::now()->timestamp.'.dmp'; + if ($this->backup->dump_all) { + $this->backup_file = '/pg-dump-all-'.Carbon::now()->timestamp.'.gz'; + } $this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ 'database_name' => $database, @@ -278,6 +274,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue $this->backup_standalone_mongodb($database); } elseif (str($databaseType)->contains('mysql')) { $this->backup_file = "/mysql-dump-$database-".Carbon::now()->timestamp.'.dmp'; + if ($this->backup->dump_all) { + $this->backup_file = '/mysql-dump-all-'.Carbon::now()->timestamp.'.gz'; + } $this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ 'database_name' => $database, @@ -287,6 +286,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue $this->backup_standalone_mysql($database); } elseif (str($databaseType)->contains('mariadb')) { $this->backup_file = "/mariadb-dump-$database-".Carbon::now()->timestamp.'.dmp'; + if ($this->backup->dump_all) { + $this->backup_file = '/mariadb-dump-all-'.Carbon::now()->timestamp.'.gz'; + } $this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ 'database_name' => $database, @@ -325,18 +327,19 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage()); throw $e; } finally { - BackupCreated::dispatch($this->team->id); + if ($this->team) { + BackupCreated::dispatch($this->team->id); + } } } private function backup_standalone_mongodb(string $databaseWithCollections): void { try { - ray($this->database->toArray()); - $url = $this->database->get_db_url(useInternal: true); + $url = $this->database->internal_db_url; if ($databaseWithCollections === 'all') { $commands[] = 'mkdir -p '.$this->backup_dir; - if (str($this->database->image)->startsWith('mongo:4.0')) { + if (str($this->database->image)->startsWith('mongo:4')) { $commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location"; } else { $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location"; @@ -351,13 +354,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue } $commands[] = 'mkdir -p '.$this->backup_dir; if ($collectionsToExclude->count() === 0) { - if (str($this->database->image)->startsWith('mongo:4.0')) { + if (str($this->database->image)->startsWith('mongo:4')) { $commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location"; } else { $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --archive > $this->backup_location"; } } else { - if (str($this->database->image)->startsWith('mongo:4.0')) { + if (str($this->database->image)->startsWith('mongo:4')) { $commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location"; } else { $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location"; @@ -369,10 +372,8 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue if ($this->backup_output === '') { $this->backup_output = null; } - ray('Backup done for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); - ray('Backup failed for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location.'\n\nError:'.$e->getMessage()); throw $e; } } @@ -381,16 +382,24 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue { try { $commands[] = 'mkdir -p '.$this->backup_dir; - $commands[] = "docker exec $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location"; + $backupCommand = 'docker exec'; + if ($this->postgres_password) { + $backupCommand .= " -e PGPASSWORD=$this->postgres_password"; + } + if ($this->backup->dump_all) { + $backupCommand .= " $this->container_name pg_dumpall --username {$this->database->postgres_user} | gzip > $this->backup_location"; + } else { + $backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location"; + } + + $commands[] = $backupCommand; $this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = trim($this->backup_output); if ($this->backup_output === '') { $this->backup_output = null; } - ray('Backup done for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); - ray('Backup failed for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location.'\n\nError:'.$e->getMessage()); throw $e; } } @@ -399,17 +408,18 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue { try { $commands[] = 'mkdir -p '.$this->backup_dir; - $commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location"; - ray($commands); + if ($this->backup->dump_all) { + $commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress | gzip > $this->backup_location"; + } else { + $commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location"; + } $this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = trim($this->backup_output); if ($this->backup_output === '') { $this->backup_output = null; } - ray('Backup done for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); - ray('Backup failed for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location.'\n\nError:'.$e->getMessage()); throw $e; } } @@ -418,17 +428,18 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue { try { $commands[] = 'mkdir -p '.$this->backup_dir; - $commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location"; - ray($commands); + if ($this->backup->dump_all) { + $commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress > $this->backup_location"; + } else { + $commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location"; + } $this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = trim($this->backup_output); if ($this->backup_output === '') { $this->backup_output = null; } - ray('Backup done for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); - ray('Backup failed for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location.'\n\nError:'.$e->getMessage()); throw $e; } } @@ -452,7 +463,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue if ($this->backup->number_of_backups_locally === 0) { $deletable = $this->backup->executions()->where('status', 'success'); } else { - $deletable = $this->backup->executions()->where('status', 'success')->orderByDesc('created_at')->skip($this->backup->number_of_backups_locally - 1); + $deletable = $this->backup->executions()->where('status', 'success')->skip($this->backup->number_of_backups_locally - 1); } foreach ($deletable->get() as $execution) { delete_backup_locally($execution->filename, $this->server); @@ -472,17 +483,35 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue $bucket = $this->s3->bucket; $endpoint = $this->s3->endpoint; $this->s3->testConnection(shouldSave: true); - if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') { + if (data_get($this->backup, 'database_type') === \App\Models\ServiceDatabase::class) { $network = $this->database->service->destination->network; } else { $network = $this->database->destination->network; } - $commands[] = "docker run --pull=always -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper"; - $commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret"; + + $fullImageName = $this->getFullImageName(); + + if (isDev()) { + if ($this->database->name === 'coolify-db') { + $backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/coolify/coolify-db-'.$this->server->ip.$this->backup_file; + $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}"; + } else { + $backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name.$this->backup_file; + $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}"; + } + } else { + $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}"; + } + if ($this->s3->isHetzner()) { + $endpointWithoutBucket = 'https://'.str($endpoint)->after('https://')->after('.')->value(); + $commands[] = "docker exec backup-of-{$this->backup->uuid} mc alias set --path=off --api=S3v4 temporary {$endpointWithoutBucket} $key $secret"; + } else { + $commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret"; + } $commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/"; instant_remote_process($commands, $this->server); + $this->add_to_backup_output('Uploaded to S3.'); - ray('Uploaded to S3. '.$this->backup_location.' to s3://'.$bucket.$this->backup_dir); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); throw $e; @@ -491,4 +520,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue instant_remote_process([$command], $this->server); } } + + private function getFullImageName(): string + { + $settings = instanceSettings(); + $helperImage = config('coolify.helper_image'); + $latestVersion = $settings->helper_version; + + return "{$helperImage}:{$latestVersion}"; + } } diff --git a/app/Jobs/DatabaseBackupStatusJob.php b/app/Jobs/DatabaseBackupStatusJob.php deleted file mode 100644 index cf240e0d7..000000000 --- a/app/Jobs/DatabaseBackupStatusJob.php +++ /dev/null @@ -1,64 +0,0 @@ -scheduledDatabaseBackups()->get(); - // if ($scheduled_backups->isEmpty()) { - // continue; - // } - // foreach ($scheduled_backups as $scheduled_backup) { - // $last_days_backups = $scheduled_backup->get_last_days_backup_status(); - // if ($last_days_backups->isEmpty()) { - // continue; - // } - // $failed = $last_days_backups->where('status', 'failed'); - // } - // } - - // $scheduled_backups = ScheduledDatabaseBackup::all(); - // $databases = collect(); - // $teams = collect(); - // foreach ($scheduled_backups as $scheduled_backup) { - // $last_days_backups = $scheduled_backup->get_last_days_backup_status(); - // if ($last_days_backups->isEmpty()) { - // continue; - // } - // $failed = $last_days_backups->where('status', 'failed'); - // $database = $scheduled_backup->database; - // $team = $database->team(); - // $teams->put($team->id, $team); - // $databases->put("{$team->id}:{$database->name}", [ - // 'failed_count' => $failed->count(), - // ]); - // } - // foreach ($databases as $name => $database) { - // [$team_id, $name] = explode(':', $name); - // $team = $teams->get($team_id); - // $team?->notify(new DailyBackup($databases)); - // } - } -} diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index 6d4720f6b..2442d5b06 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -4,6 +4,7 @@ namespace App\Jobs; use App\Actions\Application\StopApplication; use App\Actions\Database\StopDatabase; +use App\Actions\Server\CleanupDocker; use App\Actions\Service\DeleteService; use App\Actions\Service\StopService; use App\Models\Application; @@ -28,17 +29,22 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, public bool $deleteConfigurations = false) - { - } + public function __construct( + public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, + public bool $deleteConfigurations = true, + public bool $deleteVolumes = true, + public bool $dockerCleanup = true, + public bool $deleteConnectedNetworks = true + ) {} public function handle() { try { - $this->resource->forceDelete(); + $persistentStorages = collect(); switch ($this->resource->type()) { case 'application': - StopApplication::run($this->resource); + $persistentStorages = $this->resource?->persistentStorages()?->get(); + StopApplication::run($this->resource, previewDeployments: true); break; case 'standalone-postgresql': case 'standalone-redis': @@ -48,21 +54,46 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue case 'standalone-keydb': case 'standalone-dragonfly': case 'standalone-clickhouse': - StopDatabase::run($this->resource); + $persistentStorages = $this->resource?->persistentStorages()?->get(); + StopDatabase::run($this->resource, true); break; case 'service': - StopService::run($this->resource); - DeleteService::run($this->resource); + StopService::run($this->resource, true); + DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks); break; } + + if ($this->deleteVolumes && $this->resource->type() !== 'service') { + $this->resource?->delete_volumes($persistentStorages); + } if ($this->deleteConfigurations) { $this->resource?->delete_configurations(); } + + $isDatabase = $this->resource instanceof StandalonePostgresql + || $this->resource instanceof StandaloneRedis + || $this->resource instanceof StandaloneMongodb + || $this->resource instanceof StandaloneMysql + || $this->resource instanceof StandaloneMariadb + || $this->resource instanceof StandaloneKeydb + || $this->resource instanceof StandaloneDragonfly + || $this->resource instanceof StandaloneClickhouse; + $server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server'); + if (($this->dockerCleanup || $isDatabase) && $server) { + CleanupDocker::dispatch($server, true); + } + + if ($this->deleteConnectedNetworks && ! $isDatabase) { + $this->resource?->delete_connected_networks($this->resource->uuid); + } } catch (\Throwable $e) { - ray($e->getMessage()); send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage()); throw $e; } finally { + $this->resource->forceDelete(); + if ($this->dockerCleanup) { + CleanupDocker::dispatch($server, true); + } Artisan::queue('cleanup:stucked-resources'); } } diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index 32c41e99c..0d7e63dd2 100644 --- a/app/Jobs/DockerCleanupJob.php +++ b/app/Jobs/DockerCleanupJob.php @@ -10,60 +10,63 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Log; -use RuntimeException; class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public $timeout = 300; + public $timeout = 600; - public ?int $usageBefore = null; + public $tries = 1; - public function __construct(public Server $server) + public ?string $usageBefore = null; + + public function middleware(): array { + return [(new WithoutOverlapping($this->server->id))->dontRelease()]; } + public function __construct(public Server $server, public bool $manualCleanup = false) {} + public function handle(): void { try { - $isInprogress = false; - $this->server->applications()->each(function ($application) use (&$isInprogress) { - if ($application->isDeploymentInprogress()) { - $isInprogress = true; - - return; - } - }); - if ($isInprogress) { - throw new RuntimeException('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...'); - } if (! $this->server->isFunctional()) { return; } + + if ($this->manualCleanup || $this->server->settings->force_docker_cleanup) { + Log::info('DockerCleanupJob '.($this->manualCleanup ? 'manual' : 'force').' cleanup on '.$this->server->name); + CleanupDocker::run(server: $this->server); + + return; + } + $this->usageBefore = $this->server->getDiskUsage(); - ray('Usage before: '.$this->usageBefore); - if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) { - ray('Cleaning up '.$this->server->name); - CleanupDocker::run($this->server); + if (str($this->usageBefore)->isEmpty() || $this->usageBefore === null || $this->usageBefore === 0) { + Log::info('DockerCleanupJob force cleanup on '.$this->server->name); + CleanupDocker::run(server: $this->server); + + return; + } + if ($this->usageBefore >= $this->server->settings->docker_cleanup_threshold) { + CleanupDocker::run(server: $this->server); $usageAfter = $this->server->getDiskUsage(); if ($usageAfter < $this->usageBefore) { $this->server->team?->notify(new DockerCleanup($this->server, 'Saved '.($this->usageBefore - $usageAfter).'% disk space.')); - // ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name); - // send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name); Log::info('DockerCleanupJob done: Saved '.($this->usageBefore - $usageAfter).'% disk space on '.$this->server->name); } else { Log::info('DockerCleanupJob failed to save disk space on '.$this->server->name); } } else { - ray('No need to clean up '.$this->server->name); Log::info('No need to clean up '.$this->server->name); } } catch (\Throwable $e) { - send_internal_notification('DockerCleanupJob failed with: '.$e->getMessage()); - ray($e->getMessage()); + CleanupDocker::run(server: $this->server); + Log::error('DockerCleanupJob failed: '.$e->getMessage()); throw $e; } } diff --git a/app/Jobs/GithubAppPermissionJob.php b/app/Jobs/GithubAppPermissionJob.php index bab8f3a25..d483fe4c2 100644 --- a/app/Jobs/GithubAppPermissionJob.php +++ b/app/Jobs/GithubAppPermissionJob.php @@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Http; @@ -23,19 +22,7 @@ class GithubAppPermissionJob implements ShouldBeEncrypted, ShouldQueue return isDev() ? 1 : 3; } - public function __construct(public GithubApp $github_app) - { - } - - public function middleware(): array - { - return [(new WithoutOverlapping($this->github_app->uuid))]; - } - - public function uniqueId(): int - { - return $this->github_app->uuid; - } + public function __construct(public GithubApp $github_app) {} public function handle() { @@ -55,7 +42,6 @@ class GithubAppPermissionJob implements ShouldBeEncrypted, ShouldQueue $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); } catch (\Throwable $e) { send_internal_notification('GithubAppPermissionJob failed with: '.$e->getMessage()); - ray($e->getMessage()); throw $e; } } diff --git a/app/Jobs/InstanceAutoUpdateJob.php b/app/Jobs/InstanceAutoUpdateJob.php deleted file mode 100644 index bce60bbc8..000000000 --- a/app/Jobs/InstanceAutoUpdateJob.php +++ /dev/null @@ -1,30 +0,0 @@ -get('https://cdn.coollabs.io/coolify/versions.json'); - if ($response->successful()) { - $versions = $response->json(); - File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT)); - } - $latest_version = get_latest_version_of_coolify(); - instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$latest_version}"], $server, false); - - $settings = InstanceSettings::get(); - $current_version = config('version'); - if (! $settings->is_auto_update_enabled) { - return; - } - if ($latest_version === $current_version) { - return; - } - if (version_compare($latest_version, $current_version, '<')) { - return; - } - instant_remote_process([ - 'curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh', - "bash /data/coolify/source/upgrade.sh $latest_version", - ], $server); - } catch (\Throwable $e) { - throw $e; - } - } -} diff --git a/app/Jobs/PullHelperImageJob.php b/app/Jobs/PullHelperImageJob.php index d3bda2ea1..a92e44c6b 100644 --- a/app/Jobs/PullHelperImageJob.php +++ b/app/Jobs/PullHelperImageJob.php @@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue @@ -17,31 +16,12 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue public $timeout = 1000; - public function middleware(): array - { - return [(new WithoutOverlapping($this->server->uuid))]; - } - - public function uniqueId(): string - { - return $this->server->uuid; - } - - public function __construct(public Server $server) - { - } + public function __construct(public Server $server) {} public function handle(): void { - try { - $helperImage = config('coolify.helper_image'); - ray("Pulling {$helperImage}"); - instant_remote_process(["docker pull -q {$helperImage}"], $this->server, false); - ray('PullHelperImageJob done'); - } catch (\Throwable $e) { - send_internal_notification('PullHelperImageJob failed with: '.$e->getMessage()); - ray($e->getMessage()); - throw $e; - } + $helperImage = config('coolify.helper_image'); + $latest_version = instanceSettings()->helper_version; + instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false); } } diff --git a/app/Jobs/PullSentinelImageJob.php b/app/Jobs/PullSentinelImageJob.php deleted file mode 100644 index 1dd4b1dd3..000000000 --- a/app/Jobs/PullSentinelImageJob.php +++ /dev/null @@ -1,60 +0,0 @@ -server->uuid))]; - } - - public function uniqueId(): string - { - return $this->server->uuid; - } - - public function __construct(public Server $server) - { - } - - public function handle(): void - { - try { - $version = get_latest_sentinel_version(); - if (! $version) { - ray('Failed to get latest Sentinel version'); - - return; - } - $local_version = instant_remote_process(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/version"'], $this->server, false); - if (empty($local_version)) { - $local_version = '0.0.0'; - } - if (version_compare($local_version, $version, '<')) { - StartSentinel::run($this->server, $version, true); - - return; - } - ray('Sentinel image is up to date'); - } catch (\Throwable $e) { - send_internal_notification('PullSentinelImageJob failed with: '.$e->getMessage()); - ray($e->getMessage()); - throw $e; - } - } -} diff --git a/app/Jobs/PullTemplatesFromCDN.php b/app/Jobs/PullTemplatesFromCDN.php index 948060033..bde5e6c7a 100644 --- a/app/Jobs/PullTemplatesFromCDN.php +++ b/app/Jobs/PullTemplatesFromCDN.php @@ -17,26 +17,23 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue public $timeout = 10; - public function __construct() - { - } + public function __construct() {} public function handle(): void { try { - if (! isDev()) { - ray('PullTemplatesAndVersions service-templates'); - $response = Http::retry(3, 1000)->get(config('constants.services.official')); - if ($response->successful()) { - $services = $response->json(); - File::put(base_path('templates/service-templates.json'), json_encode($services)); - } else { - send_internal_notification('PullTemplatesAndVersions failed with: '.$response->status().' '.$response->body()); - } + if (isDev() || isCloud()) { + return; + } + $response = Http::retry(3, 1000)->get(config('constants.services.official')); + if ($response->successful()) { + $services = $response->json(); + File::put(base_path('templates/service-templates.json'), json_encode($services)); + } else { + send_internal_notification('PullTemplatesAndVersions failed with: '.$response->status().' '.$response->body()); } } catch (\Throwable $e) { send_internal_notification('PullTemplatesAndVersions failed with: '.$e->getMessage()); - ray($e->getMessage()); } } } diff --git a/app/Jobs/PullVersionsFromCDN.php b/app/Jobs/PullVersionsFromCDN.php deleted file mode 100644 index 1ad4989de..000000000 --- a/app/Jobs/PullVersionsFromCDN.php +++ /dev/null @@ -1,41 +0,0 @@ -get('https://cdn.coollabs.io/coolify/versions.json'); - if ($response->successful()) { - $versions = $response->json(); - File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT)); - } else { - send_internal_notification('PullTemplatesAndVersions failed with: '.$response->status().' '.$response->body()); - } - } - } catch (\Throwable $e) { - throw $e; - } - } -} diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php new file mode 100644 index 000000000..9822ca071 --- /dev/null +++ b/app/Jobs/PushServerUpdateJob.php @@ -0,0 +1,366 @@ +containers = collect(); + $this->foundApplicationIds = collect(); + $this->foundDatabaseUuids = collect(); + $this->foundServiceApplicationIds = collect(); + $this->foundApplicationPreviewsIds = collect(); + $this->foundServiceDatabaseIds = collect(); + $this->allApplicationIds = collect(); + $this->allDatabaseUuids = collect(); + $this->allTcpProxyUuids = collect(); + $this->allServiceApplicationIds = collect(); + $this->allServiceDatabaseIds = collect(); + } + + public function handle() + { + // TODO: Swarm is not supported yet + if (! $this->data) { + throw new \Exception('No data provided'); + } + $data = collect($this->data); + + $this->server->sentinelHeartbeat(); + + $this->containers = collect(data_get($data, 'containers')); + + $filesystemUsageRoot = data_get($data, 'filesystem_usage_root.used_percentage'); + ServerStorageCheckJob::dispatch($this->server, $filesystemUsageRoot); + + if ($this->containers->isEmpty()) { + return; + } + $this->applications = $this->server->applications(); + $this->databases = $this->server->databases(); + $this->previews = $this->server->previews(); + $this->services = $this->server->services()->get(); + $this->allApplicationIds = $this->applications->filter(function ($application) { + return $application->additional_servers->count() === 0; + })->pluck('id'); + $this->allApplicationsWithAdditionalServers = $this->applications->filter(function ($application) { + return $application->additional_servers->count() > 0; + }); + $this->allApplicationPreviewsIds = $this->previews->pluck('id'); + $this->allDatabaseUuids = $this->databases->pluck('uuid'); + $this->allTcpProxyUuids = $this->databases->where('is_public', true)->pluck('uuid'); + $this->services->each(function ($service) { + $service->applications()->pluck('id')->each(function ($applicationId) { + $this->allServiceApplicationIds->push($applicationId); + }); + $service->databases()->pluck('id')->each(function ($databaseId) { + $this->allServiceDatabaseIds->push($databaseId); + }); + }); + + foreach ($this->containers as $container) { + $containerStatus = data_get($container, 'state', 'exited'); + $containerHealth = data_get($container, 'health_status', 'unhealthy'); + $containerStatus = "$containerStatus ($containerHealth)"; + $labels = collect(data_get($container, 'labels')); + $coolify_managed = $labels->has('coolify.managed'); + if ($coolify_managed) { + $name = data_get($container, 'name'); + if ($name === 'coolify-log-drain' && $this->isRunning($containerStatus)) { + $this->foundLogDrainContainer = true; + } + if ($labels->has('coolify.applicationId')) { + $applicationId = $labels->get('coolify.applicationId'); + $pullRequestId = data_get($labels, 'coolify.pullRequestId', '0'); + try { + if ($pullRequestId === '0') { + if ($this->allApplicationIds->contains($applicationId) && $this->isRunning($containerStatus)) { + $this->foundApplicationIds->push($applicationId); + } + $this->updateApplicationStatus($applicationId, $containerStatus); + } else { + if ($this->allApplicationPreviewsIds->contains($applicationId) && $this->isRunning($containerStatus)) { + $this->foundApplicationPreviewsIds->push($applicationId); + } + $this->updateApplicationPreviewStatus($applicationId, $containerStatus); + } + } catch (\Exception $e) { + } + } elseif ($labels->has('coolify.serviceId')) { + $serviceId = $labels->get('coolify.serviceId'); + $subType = $labels->get('coolify.service.subType'); + $subId = $labels->get('coolify.service.subId'); + if ($subType === 'application' && $this->isRunning($containerStatus)) { + $this->foundServiceApplicationIds->push($subId); + $this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus); + } elseif ($subType === 'database' && $this->isRunning($containerStatus)) { + $this->foundServiceDatabaseIds->push($subId); + $this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus); + } + } else { + $uuid = $labels->get('com.docker.compose.service'); + $type = $labels->get('coolify.type'); + if ($name === 'coolify-proxy' && $this->isRunning($containerStatus)) { + $this->foundProxy = true; + } elseif ($type === 'service' && $this->isRunning($containerStatus)) { + } else { + if ($this->allDatabaseUuids->contains($uuid) && $this->isRunning($containerStatus)) { + $this->foundDatabaseUuids->push($uuid); + if ($this->allTcpProxyUuids->contains($uuid) && $this->isRunning($containerStatus)) { + $this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: true); + } else { + $this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: false); + } + } + } + } + } + } + + $this->updateProxyStatus(); + + $this->updateNotFoundApplicationStatus(); + $this->updateNotFoundApplicationPreviewStatus(); + $this->updateNotFoundDatabaseStatus(); + $this->updateNotFoundServiceStatus(); + + $this->updateAdditionalServersStatus(); + + $this->checkLogDrainContainer(); + } + + private function updateApplicationStatus(string $applicationId, string $containerStatus) + { + $application = $this->applications->where('id', $applicationId)->first(); + if (! $application) { + return; + } + $application->status = $containerStatus; + $application->save(); + } + + private function updateApplicationPreviewStatus(string $applicationId, string $containerStatus) + { + $application = $this->previews->where('id', $applicationId)->first(); + if (! $application) { + return; + } + $application->status = $containerStatus; + $application->save(); + } + + private function updateNotFoundApplicationStatus() + { + $notFoundApplicationIds = $this->allApplicationIds->diff($this->foundApplicationIds); + if ($notFoundApplicationIds->isNotEmpty()) { + $notFoundApplicationIds->each(function ($applicationId) { + $application = Application::find($applicationId); + if ($application) { + $application->status = 'exited'; + $application->save(); + } + }); + } + } + + private function updateNotFoundApplicationPreviewStatus() + { + $notFoundApplicationPreviewsIds = $this->allApplicationPreviewsIds->diff($this->foundApplicationPreviewsIds); + if ($notFoundApplicationPreviewsIds->isNotEmpty()) { + $notFoundApplicationPreviewsIds->each(function ($applicationPreviewId) { + $applicationPreview = ApplicationPreview::find($applicationPreviewId); + if ($applicationPreview) { + $applicationPreview->status = 'exited'; + $applicationPreview->save(); + } + }); + } + } + + private function updateProxyStatus() + { + // If proxy is not found, start it + if ($this->server->isProxyShouldRun()) { + if ($this->foundProxy === false) { + try { + if (CheckProxy::run($this->server)) { + StartProxy::run($this->server, false); + $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); + } + } catch (\Throwable $e) { + } + } else { + $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + } + } + } + + private function updateDatabaseStatus(string $databaseUuid, string $containerStatus, bool $tcpProxy = false) + { + $database = $this->databases->where('uuid', $databaseUuid)->first(); + if (! $database) { + return; + } + $database->status = $containerStatus; + $database->save(); + if ($this->isRunning($containerStatus) && $tcpProxy) { + $tcpProxyContainerFound = $this->containers->filter(function ($value, $key) use ($databaseUuid) { + return data_get($value, 'name') === "$databaseUuid-proxy" && data_get($value, 'state') === 'running'; + })->first(); + if (! $tcpProxyContainerFound) { + StartDatabaseProxy::dispatch($database); + $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); + } else { + } + } + } + + private function updateNotFoundDatabaseStatus() + { + $notFoundDatabaseUuids = $this->allDatabaseUuids->diff($this->foundDatabaseUuids); + if ($notFoundDatabaseUuids->isNotEmpty()) { + $notFoundDatabaseUuids->each(function ($databaseUuid) { + $database = $this->databases->where('uuid', $databaseUuid)->first(); + if ($database) { + $database->status = 'exited'; + $database->save(); + if ($database->is_public) { + StopDatabaseProxy::dispatch($database); + } + } + }); + } + } + + private function updateServiceSubStatus(string $serviceId, string $subType, string $subId, string $containerStatus) + { + $service = $this->services->where('id', $serviceId)->first(); + if (! $service) { + return; + } + if ($subType === 'application') { + $application = $service->applications()->where('id', $subId)->first(); + $application->status = $containerStatus; + $application->save(); + } elseif ($subType === 'database') { + $database = $service->databases()->where('id', $subId)->first(); + $database->status = $containerStatus; + $database->save(); + } else { + } + } + + private function updateNotFoundServiceStatus() + { + $notFoundServiceApplicationIds = $this->allServiceApplicationIds->diff($this->foundServiceApplicationIds); + $notFoundServiceDatabaseIds = $this->allServiceDatabaseIds->diff($this->foundServiceDatabaseIds); + if ($notFoundServiceApplicationIds->isNotEmpty()) { + $notFoundServiceApplicationIds->each(function ($serviceApplicationId) { + $application = ServiceApplication::find($serviceApplicationId); + if ($application) { + $application->status = 'exited'; + $application->save(); + } + }); + } + if ($notFoundServiceDatabaseIds->isNotEmpty()) { + $notFoundServiceDatabaseIds->each(function ($serviceDatabaseId) { + $database = ServiceDatabase::find($serviceDatabaseId); + if ($database) { + $database->status = 'exited'; + $database->save(); + } + }); + } + } + + private function updateAdditionalServersStatus() + { + $this->allApplicationsWithAdditionalServers->each(function ($application) { + ComplexStatusCheck::run($application); + }); + } + + private function isRunning(string $containerStatus) + { + return str($containerStatus)->contains('running'); + } + + private function checkLogDrainContainer() + { + if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) { + StartLogDrain::dispatch($this->server)->onQueue('high'); + } + } +} diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php index 819e28f89..7bfc29af3 100644 --- a/app/Jobs/ScheduledTaskJob.php +++ b/app/Jobs/ScheduledTaskJob.php @@ -2,6 +2,7 @@ namespace App\Jobs; +use App\Events\ScheduledTaskDone; use App\Models\Application; use App\Models\ScheduledTask; use App\Models\ScheduledTaskExecution; @@ -13,14 +14,13 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; class ScheduledTaskJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public ?Team $team = null; + public Team $team; public Server $server; @@ -36,6 +36,8 @@ class ScheduledTaskJob implements ShouldQueue public array $containers = []; + public string $server_timezone; + public function __construct($task) { $this->task = $task; @@ -46,17 +48,19 @@ class ScheduledTaskJob implements ShouldQueue } else { throw new \RuntimeException('ScheduledTaskJob failed: No resource found.'); } - $this->team = Team::find($task->team_id); + $this->team = Team::findOrFail($task->team_id); + $this->server_timezone = $this->getServerTimezone(); } - public function middleware(): array + private function getServerTimezone(): string { - return [new WithoutOverlapping($this->task->id)]; - } + if ($this->resource instanceof Application) { + return $this->resource->destination->server->settings->server_timezone; + } elseif ($this->resource instanceof Service) { + return $this->resource->server->settings->server_timezone; + } - public function uniqueId(): int - { - return $this->task->id; + return 'UTC'; } public function handle(): void @@ -68,14 +72,14 @@ class ScheduledTaskJob implements ShouldQueue $this->server = $this->resource->destination->server; - if ($this->resource->type() == 'application') { + if ($this->resource->type() === 'application') { $containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0); if ($containers->count() > 0) { $containers->each(function ($container) { $this->containers[] = str_replace('/', '', $container['Names']); }); } - } elseif ($this->resource->type() == 'service') { + } elseif ($this->resource->type() === 'service') { $this->resource->applications()->get()->each(function ($application) { if (str(data_get($application, 'status'))->contains('running')) { $this->containers[] = data_get($application, 'name').'-'.data_get($this->resource, 'uuid'); @@ -121,6 +125,8 @@ class ScheduledTaskJob implements ShouldQueue $this->team?->notify(new TaskFailed($this->task, $e->getMessage())); // send_internal_notification('ScheduledTaskJob failed with: ' . $e->getMessage()); throw $e; + } finally { + ScheduledTaskDone::dispatch($this->team->id); } } } diff --git a/app/Jobs/SendConfirmationForWaitlistJob.php b/app/Jobs/SendConfirmationForWaitlistJob.php index 4d5618df0..7af8205fc 100755 --- a/app/Jobs/SendConfirmationForWaitlistJob.php +++ b/app/Jobs/SendConfirmationForWaitlistJob.php @@ -14,14 +14,12 @@ class SendConfirmationForWaitlistJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(public string $email, public string $uuid) - { - } + public function __construct(public string $email, public string $uuid) {} public function handle() { try { - $mail = new MailMessage(); + $mail = new MailMessage; $confirmation_url = base_url().'/webhooks/waitlist/confirm?email='.$this->email.'&confirmation_code='.$this->uuid; $cancel_url = base_url().'/webhooks/waitlist/cancel?email='.$this->email.'&confirmation_code='.$this->uuid; $mail->view('emails.waitlist-confirmation', @@ -33,7 +31,6 @@ class SendConfirmationForWaitlistJob implements ShouldBeEncrypted, ShouldQueue send_user_an_email($mail, $this->email); } catch (\Throwable $e) { send_internal_notification("SendConfirmationForWaitlistJob failed for {$this->email} with error: ".$e->getMessage()); - ray($e->getMessage()); throw $e; } } diff --git a/app/Jobs/SendMessageToDiscordJob.php b/app/Jobs/SendMessageToDiscordJob.php index 90f2e0b30..5b406f50f 100644 --- a/app/Jobs/SendMessageToDiscordJob.php +++ b/app/Jobs/SendMessageToDiscordJob.php @@ -2,6 +2,7 @@ namespace App\Jobs; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; @@ -29,19 +30,15 @@ class SendMessageToDiscordJob implements ShouldBeEncrypted, ShouldQueue public int $maxExceptions = 5; public function __construct( - public string $text, + public DiscordMessage $message, public string $webhookUrl - ) { - } + ) {} /** * Execute the job. */ public function handle(): void { - $payload = [ - 'content' => $this->text, - ]; - Http::post($this->webhookUrl, $payload); + Http::post($this->webhookUrl, $this->message->toPayload()); } } diff --git a/app/Jobs/SendMessageToTelegramJob.php b/app/Jobs/SendMessageToTelegramJob.php index b81bbc50b..bf52b782f 100644 --- a/app/Jobs/SendMessageToTelegramJob.php +++ b/app/Jobs/SendMessageToTelegramJob.php @@ -33,8 +33,7 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue public string $token, public string $chatId, public ?string $topicId = null, - ) { - } + ) {} /** * Execute the job. diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php new file mode 100644 index 000000000..c584f493d --- /dev/null +++ b/app/Jobs/ServerCheckJob.php @@ -0,0 +1,103 @@ +server->id))->dontRelease()]; + } + + public function __construct(public Server $server) {} + + public function handle() + { + try { + if ($this->server->serverStatus() === false) { + return 'Server is not reachable or not ready.'; + } + + if (! $this->server->isSwarmWorker() && ! $this->server->isBuildServer()) { + ['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers(); + if (is_null($this->containers)) { + return 'No containers found.'; + } + GetContainersStatus::run($this->server, $this->containers, $containerReplicates); + + if ($this->server->isSentinelEnabled()) { + CheckAndStartSentinelJob::dispatch($this->server); + } + + if ($this->server->isLogDrainEnabled()) { + $this->checkLogDrainContainer(); + } + + if ($this->server->proxySet() && ! $this->server->proxy->force_stop) { + $this->server->proxyType(); + $foundProxyContainer = $this->containers->filter(function ($value, $key) { + if ($this->server->isSwarm()) { + return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; + } else { + return data_get($value, 'Name') === '/coolify-proxy'; + } + })->first(); + if (! $foundProxyContainer) { + try { + $shouldStart = CheckProxy::run($this->server); + if ($shouldStart) { + StartProxy::run($this->server, false); + $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); + } + } catch (\Throwable $e) { + } + } else { + $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); + $this->server->save(); + $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + } + } + } + } catch (\Throwable $e) { + return handleError($e); + } + } + + private function checkLogDrainContainer() + { + $foundLogDrainContainer = $this->containers->filter(function ($value, $key) { + return data_get($value, 'Name') === '/coolify-log-drain'; + })->first(); + if ($foundLogDrainContainer) { + $status = data_get($foundLogDrainContainer, 'State.Status'); + if ($status !== 'running') { + StartLogDrain::dispatch($this->server)->onQueue('high'); + } + } else { + StartLogDrain::dispatch($this->server)->onQueue('high'); + } + } +} diff --git a/app/Jobs/ServerCheckNewJob.php b/app/Jobs/ServerCheckNewJob.php new file mode 100644 index 000000000..3e8e60a31 --- /dev/null +++ b/app/Jobs/ServerCheckNewJob.php @@ -0,0 +1,34 @@ +server); + ResourcesCheck::dispatch($this->server); + } catch (\Throwable $e) { + return handleError($e); + } + } +} diff --git a/app/Jobs/ServerCleanupMux.php b/app/Jobs/ServerCleanupMux.php new file mode 100644 index 000000000..b793c3eca --- /dev/null +++ b/app/Jobs/ServerCleanupMux.php @@ -0,0 +1,40 @@ +server->serverStatus() === false) { + return 'Server is not reachable or not ready.'; + } + SshMultiplexingHelper::removeMuxFile($this->server); + } catch (\Throwable $e) { + return handleError($e); + } + } +} diff --git a/app/Jobs/ServerFilesFromServerJob.php b/app/Jobs/ServerFilesFromServerJob.php index 2476c12dd..769dfc004 100644 --- a/app/Jobs/ServerFilesFromServerJob.php +++ b/app/Jobs/ServerFilesFromServerJob.php @@ -16,9 +16,7 @@ class ServerFilesFromServerJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(public ServiceApplication|ServiceDatabase|Application $resource) - { - } + public function __construct(public ServiceApplication|ServiceDatabase|Application $resource) {} public function handle() { diff --git a/app/Jobs/ServerLimitCheckJob.php b/app/Jobs/ServerLimitCheckJob.php index 3eaf88ba7..aa82c6dad 100644 --- a/app/Jobs/ServerLimitCheckJob.php +++ b/app/Jobs/ServerLimitCheckJob.php @@ -10,7 +10,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue @@ -24,30 +23,15 @@ class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue return isDev() ? 1 : 3; } - public function __construct(public Team $team) - { - } - - public function middleware(): array - { - return [(new WithoutOverlapping($this->team->uuid))]; - } - - public function uniqueId(): int - { - return $this->team->uuid; - } + public function __construct(public Team $team) {} public function handle() { try { $servers = $this->team->servers; $servers_count = $servers->count(); - $limit = data_get($this->team->limits, 'serverLimit', 2); - $number_of_servers_to_disable = $servers_count - $limit; - ray('ServerLimitCheckJob', $this->team->uuid, $servers_count, $limit, $number_of_servers_to_disable); + $number_of_servers_to_disable = $servers_count - $this->team->limits; if ($number_of_servers_to_disable > 0) { - ray('Disabling servers'); $servers = $servers->sortbyDesc('created_at'); $servers_to_disable = $servers->take($number_of_servers_to_disable); $servers_to_disable->each(function ($server) { @@ -64,7 +48,6 @@ class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue } } catch (\Throwable $e) { send_internal_notification('ServerLimitCheckJob failed with: '.$e->getMessage()); - ray($e->getMessage()); return handleError($e); } diff --git a/app/Jobs/ServerStatusJob.php b/app/Jobs/ServerStatusJob.php deleted file mode 100644 index aaf8f5784..000000000 --- a/app/Jobs/ServerStatusJob.php +++ /dev/null @@ -1,137 +0,0 @@ -server->uuid))]; - } - - public function uniqueId(): int - { - return $this->server->uuid; - } - - public function handle() - { - if (! $this->server->isServerReady($this->tries)) { - throw new \RuntimeException('Server is not ready.'); - } - try { - if ($this->server->isFunctional()) { - $this->cleanup(notify: false); - $this->remove_unnecessary_coolify_yaml(); - if (config('coolify.is_sentinel_enabled')) { - $this->server->checkSentinel(); - } - } - } catch (\Throwable $e) { - send_internal_notification('ServerStatusJob failed with: '.$e->getMessage()); - ray($e->getMessage()); - - return handleError($e); - } - try { - // $this->check_docker_engine(); - } catch (\Throwable $e) { - // Do nothing - } - } - - private function check_docker_engine() - { - $version = instant_remote_process([ - 'docker info', - ], $this->server, false); - if (is_null($version)) { - $os = instant_remote_process([ - 'cat /etc/os-release | grep ^ID=', - ], $this->server, false); - $os = str($os)->after('ID=')->trim(); - if ($os === 'ubuntu') { - try { - instant_remote_process([ - 'systemctl start docker', - ], $this->server); - } catch (\Throwable $e) { - ray($e->getMessage()); - - return handleError($e); - } - } else { - try { - instant_remote_process([ - 'service docker start', - ], $this->server); - } catch (\Throwable $e) { - ray($e->getMessage()); - - return handleError($e); - } - } - } - } - - private function remove_unnecessary_coolify_yaml() - { - // This will remote the coolify.yaml file from the server as it is not needed on cloud servers - if (isCloud() && $this->server->id !== 0) { - $file = $this->server->proxyPath().'/dynamic/coolify.yaml'; - - return instant_remote_process([ - "rm -f $file", - ], $this->server, false); - } - } - - public function cleanup(bool $notify = false): void - { - $this->disk_usage = $this->server->getDiskUsage(); - if ($this->disk_usage >= $this->server->settings->cleanup_after_percentage) { - if ($notify) { - if ($this->server->high_disk_usage_notification_sent) { - ray('high disk usage notification already sent'); - - return; - } else { - $this->server->high_disk_usage_notification_sent = true; - $this->server->save(); - $this->server->team?->notify(new HighDiskUsage($this->server, $this->disk_usage, $this->server->settings->cleanup_after_percentage)); - } - } else { - DockerCleanupJob::dispatchSync($this->server); - $this->cleanup(notify: true); - } - } else { - $this->server->high_disk_usage_notification_sent = false; - $this->server->save(); - } - } -} diff --git a/app/Jobs/ServerStorageCheckJob.php b/app/Jobs/ServerStorageCheckJob.php new file mode 100644 index 000000000..0723ffcee --- /dev/null +++ b/app/Jobs/ServerStorageCheckJob.php @@ -0,0 +1,65 @@ +server->isFunctional() === false) { + return 'Server is not functional.'; + } + $team = data_get($this->server, 'team'); + $serverDiskUsageNotificationThreshold = data_get($this->server, 'settings.server_disk_usage_notification_threshold'); + + if (is_null($this->percentage)) { + $this->percentage = $this->server->storageCheck(); + } + if (! $this->percentage) { + return 'No percentage could be retrieved.'; + } + if ($this->percentage > $serverDiskUsageNotificationThreshold) { + $executed = RateLimiter::attempt( + 'high-disk-usage:'.$this->server->id, + $maxAttempts = 0, + function () use ($team, $serverDiskUsageNotificationThreshold) { + $team->notify(new HighDiskUsage($this->server, $this->percentage, $serverDiskUsageNotificationThreshold)); + }, + $decaySeconds = 3600, + ); + + if (! $executed) { + return 'Too many messages sent!'; + } + } else { + RateLimiter::hit('high-disk-usage:'.$this->server->id, 600); + } + } catch (\Throwable $e) { + return handleError($e); + } + } +} diff --git a/app/Jobs/ServerStorageSaveJob.php b/app/Jobs/ServerStorageSaveJob.php index c94a3edc5..526cd5375 100644 --- a/app/Jobs/ServerStorageSaveJob.php +++ b/app/Jobs/ServerStorageSaveJob.php @@ -14,9 +14,7 @@ class ServerStorageSaveJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(public LocalFileVolume $localFileVolume) - { - } + public function __construct(public LocalFileVolume $localFileVolume) {} public function handle() { diff --git a/app/Jobs/SubscriptionInvoiceFailedJob.php b/app/Jobs/SubscriptionInvoiceFailedJob.php index e4cd219c8..aabeecef5 100755 --- a/app/Jobs/SubscriptionInvoiceFailedJob.php +++ b/app/Jobs/SubscriptionInvoiceFailedJob.php @@ -15,28 +15,24 @@ class SubscriptionInvoiceFailedJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(protected Team $team) - { - } + public function __construct(protected Team $team) {} public function handle() { try { $session = getStripeCustomerPortalSession($this->team); - $mail = new MailMessage(); + $mail = new MailMessage; $mail->view('emails.subscription-invoice-failed', [ 'stripeCustomerPortal' => $session->url, ]); $mail->subject('Your last payment was failed for Coolify Cloud.'); $this->team->members()->each(function ($member) use ($mail) { - ray($member); if ($member->isAdmin()) { send_user_an_email($mail, $member->email); } }); } catch (\Throwable $e) { send_internal_notification('SubscriptionInvoiceFailedJob failed with: '.$e->getMessage()); - ray($e->getMessage()); throw $e; } } diff --git a/app/Jobs/SubscriptionTrialEndedJob.php b/app/Jobs/SubscriptionTrialEndedJob.php deleted file mode 100755 index ee260d8d9..000000000 --- a/app/Jobs/SubscriptionTrialEndedJob.php +++ /dev/null @@ -1,45 +0,0 @@ -team); - $mail = new MailMessage(); - $mail->subject('Action required: You trial in Coolify Cloud ended.'); - $mail->view('emails.trial-ended', [ - 'stripeCustomerPortal' => $session->url, - ]); - $this->team->members()->each(function ($member) use ($mail) { - if ($member->isAdmin()) { - ray('Sending trial ended email to '.$member->email); - send_user_an_email($mail, $member->email); - send_internal_notification('Trial reminder email sent to '.$member->email); - } - }); - } catch (\Throwable $e) { - send_internal_notification('SubscriptionTrialEndsSoonJob failed with: '.$e->getMessage()); - ray($e->getMessage()); - throw $e; - } - } -} diff --git a/app/Jobs/SubscriptionTrialEndsSoonJob.php b/app/Jobs/SubscriptionTrialEndsSoonJob.php deleted file mode 100755 index fba668108..000000000 --- a/app/Jobs/SubscriptionTrialEndsSoonJob.php +++ /dev/null @@ -1,45 +0,0 @@ -team); - $mail = new MailMessage(); - $mail->subject('You trial in Coolify Cloud ends soon.'); - $mail->view('emails.trial-ends-soon', [ - 'stripeCustomerPortal' => $session->url, - ]); - $this->team->members()->each(function ($member) use ($mail) { - if ($member->isAdmin()) { - ray('Sending trial ending email to '.$member->email); - send_user_an_email($mail, $member->email); - send_internal_notification('Trial reminder email sent to '.$member->email); - } - }); - } catch (\Throwable $e) { - send_internal_notification('SubscriptionTrialEndsSoonJob failed with: '.$e->getMessage()); - ray($e->getMessage()); - throw $e; - } - } -} diff --git a/app/Jobs/UpdateCoolifyJob.php b/app/Jobs/UpdateCoolifyJob.php new file mode 100644 index 000000000..1e5197b6f --- /dev/null +++ b/app/Jobs/UpdateCoolifyJob.php @@ -0,0 +1,49 @@ +new_version_available) { + Log::info('No new version available. Skipping update.'); + + return; + } + + $server = Server::findOrFail(0); + if (! $server) { + Log::error('Server not found. Cannot proceed with update.'); + + return; + } + + Log::info('Starting Coolify update process...'); + UpdateCoolify::run(false); // false means it's not a manual update + + $settings->update(['new_version_available' => false]); + Log::info('Coolify update completed successfully.'); + } catch (\Throwable $e) { + Log::error('UpdateCoolifyJob failed: '.$e->getMessage()); + // Consider implementing a notification to administrators + } + } +} diff --git a/app/Listeners/MaintenanceModeDisabledNotification.php b/app/Listeners/MaintenanceModeDisabledNotification.php index 9f676ca99..6c3ab83d8 100644 --- a/app/Listeners/MaintenanceModeDisabledNotification.php +++ b/app/Listeners/MaintenanceModeDisabledNotification.php @@ -9,13 +9,10 @@ use Symfony\Component\HttpFoundation\Request as SymfonyRequest; class MaintenanceModeDisabledNotification { - public function __construct() - { - } + public function __construct() {} public function handle(EventsMaintenanceModeDisabled $event): void { - ray('Maintenance mode disabled!'); $files = Storage::disk('webhooks-during-maintenance')->files(); $files = collect($files); $files = $files->sort(); @@ -40,10 +37,9 @@ class MaintenanceModeDisabledNotification $class = "App\Http\Controllers\Webhook\\".ucfirst(str($endpoint)->before('::')->value()); $method = str($endpoint)->after('::')->value(); try { - $instance = new $class(); + $instance = new $class; $instance->$method($request); } catch (\Throwable $th) { - ray($th); } finally { Storage::disk('webhooks-during-maintenance')->delete($file); } diff --git a/app/Listeners/MaintenanceModeEnabledNotification.php b/app/Listeners/MaintenanceModeEnabledNotification.php index b2cd8c738..5aab248ea 100644 --- a/app/Listeners/MaintenanceModeEnabledNotification.php +++ b/app/Listeners/MaintenanceModeEnabledNotification.php @@ -17,8 +17,5 @@ class MaintenanceModeEnabledNotification /** * Handle the event. */ - public function handle(EventsMaintenanceModeEnabled $event): void - { - ray('Maintenance mode enabled!'); - } + public function handle(EventsMaintenanceModeEnabled $event): void {} } diff --git a/app/Listeners/ProxyStartedNotification.php b/app/Listeners/ProxyStartedNotification.php index 64271cc52..d0541b162 100644 --- a/app/Listeners/ProxyStartedNotification.php +++ b/app/Listeners/ProxyStartedNotification.php @@ -9,9 +9,7 @@ class ProxyStartedNotification { public Server $server; - public function __construct() - { - } + public function __construct() {} public function handle(ProxyStarted $event): void { diff --git a/app/Livewire/ActivityMonitor.php b/app/Livewire/ActivityMonitor.php index bd1e30088..2e36f34ee 100644 --- a/app/Livewire/ActivityMonitor.php +++ b/app/Livewire/ActivityMonitor.php @@ -2,7 +2,6 @@ namespace App\Livewire; -use App\Enums\ProcessStatus; use App\Models\User; use Livewire\Component; use Spatie\Activitylog\Models\Activity; diff --git a/app/Livewire/Admin/Index.php b/app/Livewire/Admin/Index.php index 26b31e515..359db6329 100644 --- a/app/Livewire/Admin/Index.php +++ b/app/Livewire/Admin/Index.php @@ -2,76 +2,60 @@ namespace App\Livewire\Admin; +use App\Models\Team; use App\Models\User; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; use Livewire\Component; class Index extends Component { - public $active_subscribers = []; + public int $activeSubscribers; - public $inactive_subscribers = []; + public int $inactiveSubscribers; - public $search = ''; + public Collection $foundUsers; - public function submitSearch() - { - if ($this->search !== '') { - $this->inactive_subscribers = User::whereDoesntHave('teams', function ($query) { - $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null); - })->where(function ($query) { - $query->where('name', 'like', "%{$this->search}%") - ->orWhere('email', 'like', "%{$this->search}%"); - })->get()->filter(function ($user) { - return $user->id !== 0; - }); - $this->active_subscribers = User::whereHas('teams', function ($query) { - $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null); - })->where(function ($query) { - $query->where('name', 'like', "%{$this->search}%") - ->orWhere('email', 'like', "%{$this->search}%"); - })->get()->filter(function ($user) { - return $user->id !== 0; - }); - } else { - $this->getSubscribers(); - } - } + public string $search = ''; public function mount() { if (! isCloud()) { return redirect()->route('dashboard'); } - if (auth()->user()->id !== 0) { + + if (Auth::id() !== 0) { return redirect()->route('dashboard'); } $this->getSubscribers(); } + public function submitSearch() + { + if ($this->search !== '') { + $this->foundUsers = User::where(function ($query) { + $query->where('name', 'like', "%{$this->search}%") + ->orWhere('email', 'like', "%{$this->search}%"); + })->get(); + } + } + public function getSubscribers() { - $this->inactive_subscribers = User::whereDoesntHave('teams', function ($query) { - $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null); - })->get()->filter(function ($user) { - return $user->id !== 0; - }); - $this->active_subscribers = User::whereHas('teams', function ($query) { - $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null); - })->get()->filter(function ($user) { - return $user->id !== 0; - }); + $this->inactiveSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', false)->count(); + $this->activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->count(); } public function switchUser(int $user_id) { - if (auth()->user()->id !== 0) { + if (Auth::id() !== 0) { return redirect()->route('dashboard'); } $user = User::find($user_id); $team_to_switch_to = $user->teams->first(); Cache::forget("team:{$user->id}"); - auth()->login($user); + Auth::login($user); refreshSession($team_to_switch_to); return redirect(request()->header('Referer')); diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index b787ed0cc..c9c3092b3 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -12,7 +12,7 @@ use Livewire\Component; class Index extends Component { - protected $listeners = ['serverInstalled' => 'validateServer']; + protected $listeners = ['refreshBoardingIndex' => 'validateServer']; public string $currentState = 'welcome'; @@ -85,26 +85,6 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== $this->remoteServerDescription = 'Created by Coolify'; $this->remoteServerHost = 'coolify-testing-host'; } - // if ($this->currentState === 'create-project') { - // $this->getProjects(); - // } - // if ($this->currentState === 'create-resource') { - // $this->selectExistingServer(); - // $this->selectExistingProject(); - // } - // if ($this->currentState === 'private-key') { - // $this->setServerType('remote'); - // } - // if ($this->currentState === 'create-server') { - // $this->selectExistingPrivateKey(); - // } - // if ($this->currentState === 'validate-server') { - // $this->selectExistingServer(); - // } - // if ($this->currentState === 'select-existing-server') { - // $this->selectExistingServer(); - // } - } public function explanation() @@ -139,7 +119,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== if (! $this->createdServer) { return $this->dispatch('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.'); } - $this->serverPublicKey = $this->createdServer->privateKey->publicKey(); + $this->serverPublicKey = $this->createdServer->privateKey->getPublicKey(); return $this->validateServer('localhost'); } elseif ($this->selectedServerType === 'remote') { @@ -154,6 +134,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== $this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get(); if ($this->servers->count() > 0) { $this->selectedExistingServer = $this->servers->first()->id; + $this->updateServerDetails(); $this->currentState = 'select-existing-server'; return; @@ -172,14 +153,23 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== return; } $this->selectedExistingPrivateKey = $this->createdServer->privateKey->id; - $this->serverPublicKey = $this->createdServer->privateKey->publicKey(); + $this->serverPublicKey = $this->createdServer->privateKey->getPublicKey(); + $this->updateServerDetails(); $this->currentState = 'validate-server'; } + private function updateServerDetails() + { + if ($this->createdServer) { + $this->remoteServerPort = $this->createdServer->port; + $this->remoteServerUser = $this->createdServer->user; + } + } + public function getProxyType() { // Set Default Proxy Type - $this->selectProxy(ProxyTypes::TRAEFIK_V2->value); + $this->selectProxy(ProxyTypes::TRAEFIK->value); // $proxyTypeSet = $this->createdServer->proxy->type; // if (!$proxyTypeSet) { // $this->currentState = 'select-proxy'; @@ -219,27 +209,35 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== public function savePrivateKey() { $this->validate([ - 'privateKeyName' => 'required', - 'privateKey' => 'required', + 'privateKeyName' => 'required|string|max:255', + 'privateKeyDescription' => 'nullable|string|max:255', + 'privateKey' => 'required|string', ]); - $this->createdPrivateKey = PrivateKey::create([ - 'name' => $this->privateKeyName, - 'description' => $this->privateKeyDescription, - 'private_key' => $this->privateKey, - 'team_id' => currentTeam()->id, - ]); - $this->createdPrivateKey->save(); - $this->currentState = 'create-server'; + + try { + $privateKey = PrivateKey::createAndStore([ + 'name' => $this->privateKeyName, + 'description' => $this->privateKeyDescription, + 'private_key' => $this->privateKey, + 'team_id' => currentTeam()->id, + ]); + + $this->createdPrivateKey = $privateKey; + $this->currentState = 'create-server'; + } catch (\Exception $e) { + $this->addError('privateKey', 'Failed to save private key: '.$e->getMessage()); + } } public function saveServer() { $this->validate([ - 'remoteServerName' => 'required', - 'remoteServerHost' => 'required', + 'remoteServerName' => 'required|string', + 'remoteServerHost' => 'required|string', 'remoteServerPort' => 'required|integer', - 'remoteServerUser' => 'required', + 'remoteServerUser' => 'required|string', ]); + $this->privateKey = formatPrivateKey($this->privateKey); $foundServer = Server::whereIp($this->remoteServerHost)->first(); if ($foundServer) { @@ -257,7 +255,6 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== $this->createdServer->settings->is_swarm_manager = $this->isSwarmManager; $this->createdServer->settings->is_cloudflare_tunnel = $this->isCloudflareTunnel; $this->createdServer->settings->save(); - $this->createdServer->addInitialNetwork(); $this->selectedExistingServer = $this->createdServer->id; $this->currentState = 'validate-server'; } @@ -270,7 +267,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== public function validateServer() { try { - config()->set('coolify.mux_enabled', false); + config()->set('constants.ssh.mux_enabled', false); // EC2 does not have `uptime` command, lol instant_remote_process(['ls /'], $this->createdServer, true); @@ -278,9 +275,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== $this->createdServer->settings()->update([ 'is_reachable' => true, ]); + $this->serverReachable = true; } catch (\Throwable $e) { $this->serverReachable = false; - $this->createdServer->delete(); + $this->createdServer->settings()->update([ + 'is_reachable' => false, + ]); return handleError(error: $e, livewire: $this); } @@ -297,6 +297,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== ]); $this->getProxyType(); } catch (\Throwable $e) { + $this->createdServer->settings()->update([ + 'is_usable' => false, + ]); + return handleError(error: $e, livewire: $this); } } @@ -350,6 +354,21 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== ); } + public function saveAndValidateServer() + { + $this->validate([ + 'remoteServerPort' => 'required|integer|min:1|max:65535', + 'remoteServerUser' => 'required|string', + ]); + + $this->createdServer->update([ + 'port' => $this->remoteServerPort, + 'user' => $this->remoteServerUser, + 'timezone' => 'UTC', + ]); + $this->validateServer(); + } + private function createNewPrivateKey() { $this->privateKeyName = generate_random_name(); diff --git a/app/Livewire/CommandCenter/Index.php b/app/Livewire/CommandCenter/Index.php deleted file mode 100644 index 0a05e811f..000000000 --- a/app/Livewire/CommandCenter/Index.php +++ /dev/null @@ -1,21 +0,0 @@ -servers = Server::isReachable()->get(); - } - - public function render() - { - return view('livewire.command-center.index'); - } -} diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php index 1abd28c3c..69ba19e40 100644 --- a/app/Livewire/Dashboard.php +++ b/app/Livewire/Dashboard.php @@ -16,29 +16,28 @@ class Dashboard extends Component public Collection $servers; - public Collection $private_keys; + public Collection $privateKeys; - public $deployments_per_server; + public array $deploymentsPerServer = []; public function mount() { - $this->private_keys = PrivateKey::ownedByCurrentTeam()->get(); + $this->privateKeys = PrivateKey::ownedByCurrentTeam()->get(); $this->servers = Server::ownedByCurrentTeam()->get(); $this->projects = Project::ownedByCurrentTeam()->get(); - $this->get_deployments(); + $this->loadDeployments(); } - public function cleanup_queue() + public function cleanupQueue() { - $this->dispatch('success', 'Cleanup started.'); - Artisan::queue('cleanup:application-deployment-queue', [ + Artisan::queue('cleanup:deployment-queue', [ '--team-id' => currentTeam()->id, ]); } - public function get_deployments() + public function loadDeployments() { - $this->deployments_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $this->servers->pluck('id'))->get([ + $this->deploymentsPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $this->servers->pluck('id'))->get([ 'id', 'application_id', 'application_name', @@ -50,15 +49,6 @@ class Dashboard extends Component ])->sortBy('id')->groupBy('server_name')->toArray(); } - // public function getIptables() - // { - // $servers = Server::ownedByCurrentTeam()->get(); - // foreach ($servers as $server) { - // checkRequiredCommands($server); - // $iptables = instant_remote_process(['docker run --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c "iptables -L -n | jc --iptables"'], $server); - // ray($iptables); - // } - // } public function render() { return view('livewire.dashboard'); diff --git a/app/Livewire/Destination/Form.php b/app/Livewire/Destination/Form.php deleted file mode 100644 index 7125f2120..000000000 --- a/app/Livewire/Destination/Form.php +++ /dev/null @@ -1,46 +0,0 @@ - 'required', - 'destination.network' => 'required', - 'destination.server.ip' => 'required', - ]; - - protected $validationAttributes = [ - 'destination.name' => 'name', - 'destination.network' => 'network', - 'destination.server.ip' => 'IP Address/Domain', - ]; - - public function submit() - { - $this->validate(); - $this->destination->save(); - } - - public function delete() - { - try { - if ($this->destination->getMorphClass() === 'App\Models\StandaloneDocker') { - if ($this->destination->attachedTo()) { - return $this->dispatch('error', 'You must delete all resources before deleting this destination.'); - } - instant_remote_process(["docker network disconnect {$this->destination->network} coolify-proxy"], $this->destination->server, throwError: false); - instant_remote_process(['docker network rm -f '.$this->destination->network], $this->destination->server); - } - $this->destination->delete(); - - return redirect()->route('dashboard'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } -} diff --git a/app/Livewire/Destination/Index.php b/app/Livewire/Destination/Index.php new file mode 100644 index 000000000..a3df3fd56 --- /dev/null +++ b/app/Livewire/Destination/Index.php @@ -0,0 +1,23 @@ +servers = Server::isUsable()->get(); + } + + public function render() + { + return view('livewire.destination.index'); + } +} diff --git a/app/Livewire/Destination/New/Docker.php b/app/Livewire/Destination/New/Docker.php index f822cfa5f..f86f42e34 100644 --- a/app/Livewire/Destination/New/Docker.php +++ b/app/Livewire/Destination/New/Docker.php @@ -3,111 +3,91 @@ namespace App\Livewire\Destination\New; use App\Models\Server; -use App\Models\StandaloneDocker as ModelsStandaloneDocker; +use App\Models\StandaloneDocker; use App\Models\SwarmDocker; -use Illuminate\Support\Collection; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; use Visus\Cuid2\Cuid2; class Docker extends Component { + #[Locked] + public $servers; + + #[Locked] + public Server $selectedServer; + + #[Validate(['required', 'string'])] public string $name; + #[Validate(['required', 'string'])] public string $network; - public ?Collection $servers = null; + #[Validate(['required', 'string'])] + public string $serverId; - public Server $server; + #[Validate(['required', 'boolean'])] + public bool $isSwarm = false; - public ?int $server_id = null; - - public bool $is_swarm = false; - - protected $rules = [ - 'name' => 'required|string', - 'network' => 'required|string', - 'server_id' => 'required|integer', - 'is_swarm' => 'boolean', - ]; - - protected $validationAttributes = [ - 'name' => 'name', - 'network' => 'network', - 'server_id' => 'server', - 'is_swarm' => 'swarm', - ]; - - public function mount() + public function mount(?string $server_id = null) { - if (is_null($this->servers)) { - $this->servers = Server::isReachable()->get(); - } - if (request()->query('server_id')) { - $this->server_id = request()->query('server_id'); + $this->network = new Cuid2; + $this->servers = Server::isUsable()->get(); + if ($server_id) { + $this->selectedServer = $this->servers->find($server_id); + $this->serverId = $this->selectedServer->id; } else { - if ($this->servers->count() > 0) { - $this->server_id = $this->servers->first()->id; - } - } - if (request()->query('network_name')) { - $this->network = request()->query('network_name'); - } else { - $this->network = new Cuid2(7); - } - if ($this->servers->count() > 0) { - $this->name = str("{$this->servers->first()->name}-{$this->network}")->kebab(); + $this->selectedServer = $this->servers->first(); + $this->serverId = $this->selectedServer->id; } + $this->generateName(); } - public function generate_name() + public function updatedServerId() { - $this->server = Server::find($this->server_id); - $this->name = str("{$this->server->name}-{$this->network}")->kebab(); + $this->selectedServer = $this->servers->find($this->serverId); + $this->generateName(); + } + + public function generateName() + { + $name = data_get($this->selectedServer, 'name', new Cuid2); + $this->name = str("{$name}-{$this->network}")->kebab(); } public function submit() { - $this->validate(); try { - $this->server = Server::find($this->server_id); - if ($this->is_swarm) { - $found = $this->server->swarmDockers()->where('network', $this->network)->first(); + $this->validate(); + if ($this->isSwarm) { + $found = $this->selectedServer->swarmDockers()->where('network', $this->network)->first(); if ($found) { - $this->dispatch('error', 'Network already added to this server.'); - - return; + throw new \Exception('Network already added to this server.'); } else { $docker = SwarmDocker::create([ 'name' => $this->name, 'network' => $this->network, - 'server_id' => $this->server_id, + 'server_id' => $this->selectedServer->id, ]); } } else { - $found = $this->server->standaloneDockers()->where('network', $this->network)->first(); + $found = $this->selectedServer->standaloneDockers()->where('network', $this->network)->first(); if ($found) { - $this->dispatch('error', 'Network already added to this server.'); - - return; + throw new \Exception('Network already added to this server.'); } else { - $docker = ModelsStandaloneDocker::create([ + $docker = StandaloneDocker::create([ 'name' => $this->name, 'network' => $this->network, - 'server_id' => $this->server_id, + 'server_id' => $this->selectedServer->id, ]); } } - $this->createNetworkAndAttachToProxy(); - - return redirect()->route('destination.show', $docker->uuid); + $connectProxyToDockerNetworks = connectProxyToNetworks($this->selectedServer); + instant_remote_process($connectProxyToDockerNetworks, $this->selectedServer, false); + $this->dispatch('reloadWindow'); } catch (\Throwable $e) { return handleError($e, $this); } } - - private function createNetworkAndAttachToProxy() - { - $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); - instant_remote_process($connectProxyToDockerNetworks, $this->server, false); - } } diff --git a/app/Livewire/Destination/Show.php b/app/Livewire/Destination/Show.php index 5650e82ba..5c4d6c170 100644 --- a/app/Livewire/Destination/Show.php +++ b/app/Livewire/Destination/Show.php @@ -5,71 +5,91 @@ namespace App\Livewire\Destination; use App\Models\Server; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; -use Illuminate\Support\Collection; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class Show extends Component { - public Server $server; + #[Locked] + public $destination; - public Collection|array $networks = []; + #[Validate(['string', 'required'])] + public string $name; - private function createNetworkAndAttachToProxy() + #[Validate(['string', 'required'])] + public string $network; + + #[Validate(['string', 'required'])] + public string $serverIp; + + public function mount(string $destination_uuid) { - $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); - instant_remote_process($connectProxyToDockerNetworks, $this->server, false); - } + try { + $destination = StandaloneDocker::whereUuid($destination_uuid)->first() ?? + SwarmDocker::whereUuid($destination_uuid)->firstOrFail(); - public function add($name) - { - if ($this->server->isSwarm()) { - $found = $this->server->swarmDockers()->where('network', $name)->first(); - if ($found) { - $this->dispatch('error', 'Network already added to this server.'); - - return; - } else { - SwarmDocker::create([ - 'name' => $this->server->name.'-'.$name, - 'network' => $this->name, - 'server_id' => $this->server->id, - ]); + $ownedByTeam = Server::ownedByCurrentTeam()->each(function ($server) use ($destination) { + if ($server->standaloneDockers->contains($destination) || $server->swarmDockers->contains($destination)) { + $this->destination = $destination; + $this->syncData(); + } + }); + if ($ownedByTeam === false) { + return redirect()->route('destination.index'); } - } else { - $found = $this->server->standaloneDockers()->where('network', $name)->first(); - if ($found) { - $this->dispatch('error', 'Network already added to this server.'); - - return; - } else { - StandaloneDocker::create([ - 'name' => $this->server->name.'-'.$name, - 'network' => $name, - 'server_id' => $this->server->id, - ]); - } - $this->createNetworkAndAttachToProxy(); + $this->destination = $destination; + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); } } - public function scan() + public function syncData(bool $toModel = false) { - if ($this->server->isSwarm()) { - $alreadyAddedNetworks = $this->server->swarmDockers; + if ($toModel) { + $this->validate(); + $this->destination->name = $this->name; + $this->destination->network = $this->network; + $this->destination->server->ip = $this->serverIp; + $this->destination->save(); } else { - $alreadyAddedNetworks = $this->server->standaloneDockers; + $this->name = $this->destination->name; + $this->network = $this->destination->network; + $this->serverIp = $this->destination->server->ip; } - $networks = instant_remote_process(['docker network ls --format "{{json .}}"'], $this->server, false); - $this->networks = format_docker_command_output_to_json($networks)->filter(function ($network) { - return $network['Name'] !== 'bridge' && $network['Name'] !== 'host' && $network['Name'] !== 'none'; - })->filter(function ($network) use ($alreadyAddedNetworks) { - return ! $alreadyAddedNetworks->contains('network', $network['Name']); - }); - if ($this->networks->count() === 0) { - $this->dispatch('success', 'No new networks found.'); + } - return; + public function submit() + { + try { + $this->syncData(true); + $this->dispatch('success', 'Destination saved.'); + } catch (\Throwable $e) { + return handleError($e, $this); } - $this->dispatch('success', 'Scan done.'); + } + + public function delete() + { + try { + if ($this->destination->getMorphClass() === \App\Models\StandaloneDocker::class) { + if ($this->destination->attachedTo()) { + return $this->dispatch('error', 'You must delete all resources before deleting this destination.'); + } + instant_remote_process(["docker network disconnect {$this->destination->network} coolify-proxy"], $this->destination->server, throwError: false); + instant_remote_process(['docker network rm -f '.$this->destination->network], $this->destination->server); + } + $this->destination->delete(); + + return redirect()->route('destination.index'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.destination.show'); } } diff --git a/app/Livewire/ForcePasswordReset.php b/app/Livewire/ForcePasswordReset.php index a732ef1c9..61a2a20e9 100644 --- a/app/Livewire/ForcePasswordReset.php +++ b/app/Livewire/ForcePasswordReset.php @@ -4,6 +4,7 @@ namespace App\Livewire; use DanHarrin\LivewireRateLimiting\WithRateLimiting; use Illuminate\Support\Facades\Hash; +use Illuminate\Validation\Rules\Password; use Livewire\Component; class ForcePasswordReset extends Component @@ -16,14 +17,19 @@ class ForcePasswordReset extends Component public string $password_confirmation; - protected $rules = [ - 'email' => 'required|email', - 'password' => 'required|min:8', - 'password_confirmation' => 'required|same:password', - ]; + public function rules(): array + { + return [ + 'email' => ['required', 'email'], + 'password' => ['required', Password::defaults(), 'confirmed'], + ]; + } public function mount() { + if (auth()->user()->force_password_reset === false) { + return redirect()->route('dashboard'); + } $this->email = auth()->user()->email; } @@ -34,6 +40,10 @@ class ForcePasswordReset extends Component public function submit() { + if (auth()->user()->force_password_reset === false) { + return redirect()->route('dashboard'); + } + try { $this->rateLimit(10); $this->validate(); diff --git a/app/Livewire/Help.php b/app/Livewire/Help.php index 2fbd2bc7e..f51527fbe 100644 --- a/app/Livewire/Help.php +++ b/app/Livewire/Help.php @@ -2,59 +2,42 @@ namespace App\Livewire; -use App\Models\InstanceSettings; use DanHarrin\LivewireRateLimiting\WithRateLimiting; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Support\Facades\Http; -use Illuminate\Support\Facades\Route; +use Livewire\Attributes\Validate; use Livewire\Component; class Help extends Component { use WithRateLimiting; + #[Validate(['required', 'min:10', 'max:1000'])] public string $description; + #[Validate(['required', 'min:3'])] public string $subject; - public ?string $path = null; - - protected $rules = [ - 'description' => 'required|min:10', - 'subject' => 'required|min:3', - ]; - - public function mount() - { - $this->path = Route::current()?->uri() ?? null; - if (isDev()) { - $this->description = "I'm having trouble with {$this->path}"; - $this->subject = "Help with {$this->path}"; - } - } - public function submit() { try { - $this->rateLimit(3, 30); $this->validate(); - $debug = "Route: {$this->path}"; - $mail = new MailMessage(); + $this->rateLimit(3, 30); + + $settings = instanceSettings(); + $mail = new MailMessage; $mail->view( 'emails.help', [ 'description' => $this->description, - 'debug' => $debug, ] ); $mail->subject("[HELP]: {$this->subject}"); - $settings = InstanceSettings::get(); $type = set_transanctional_email_settings($settings); - if (! $type) { + + // Sending feedback through Cloud API + if ($type === false) { $url = 'https://app.coolify.io/api/feedback'; - if (isDev()) { - $url = 'http://localhost:80/api/feedback'; - } Http::post($url, [ 'content' => 'User: `'.auth()->user()?->email.'` with subject: `'.$this->subject.'` has the following problem: `'.$this->description.'`', ]); @@ -62,6 +45,7 @@ class Help extends Component send_user_an_email($mail, auth()->user()?->email, 'hi@coollabs.io'); } $this->dispatch('success', 'Feedback sent.', 'We will get in touch with you as soon as possible.'); + $this->reset('description', 'subject'); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/MonacoEditor.php b/app/Livewire/MonacoEditor.php new file mode 100644 index 000000000..42d276e64 --- /dev/null +++ b/app/Livewire/MonacoEditor.php @@ -0,0 +1,51 @@ + '$refresh', + ]; + + public function __construct( + public ?string $id, + public ?string $name, + public ?string $type, + public ?string $monacoContent, + public ?string $value, + public ?string $label, + public ?string $placeholder, + public bool $required, + public bool $disabled, + public bool $readonly, + public bool $allowTab, + public bool $spellcheck, + public ?string $helper, + public bool $realtimeValidation, + public bool $allowToPeak, + public string $defaultClass, + public string $defaultClassInput, + public ?string $language + + ) { + // + } + + public function render() + { + if (is_null($this->id)) { + $this->id = new Cuid2; + } + + if (is_null($this->name)) { + $this->name = $this->id; + } + + return view('components.forms.monaco-editor'); + } +} diff --git a/app/Livewire/NavbarDeleteTeam.php b/app/Livewire/NavbarDeleteTeam.php new file mode 100644 index 000000000..cc5d78f60 --- /dev/null +++ b/app/Livewire/NavbarDeleteTeam.php @@ -0,0 +1,54 @@ +team = currentTeam()->name; + } + + public function delete($password) + { + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); + + return; + } + } + + $currentTeam = currentTeam(); + $currentTeam->delete(); + + $currentTeam->members->each(function ($user) use ($currentTeam) { + if ($user->id === AttributesAuth::id()) { + return; + } + $user->teams()->detach($currentTeam); + $session = DB::table('sessions')->where('user_id', $user->id)->first(); + if ($session) { + DB::table('sessions')->where('id', $session->id)->delete(); + } + }); + + refreshSession(); + + return redirect()->route('team.index'); + } + + public function render() + { + return view('livewire.navbar-delete-team'); + } +} diff --git a/app/Livewire/NewActivityMonitor.php b/app/Livewire/NewActivityMonitor.php index 10dbb9ce7..a9334e710 100644 --- a/app/Livewire/NewActivityMonitor.php +++ b/app/Livewire/NewActivityMonitor.php @@ -68,7 +68,6 @@ class NewActivityMonitor extends Component } else { $this->dispatch($this->eventToDispatch); } - ray('Dispatched event: '.$this->eventToDispatch.' with data: '.$this->eventData); } } } diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php index f2219bbc6..7a177a227 100644 --- a/app/Livewire/Notifications/Discord.php +++ b/app/Livewire/Notifications/Discord.php @@ -4,60 +4,124 @@ namespace App\Livewire\Notifications; use App\Models\Team; use App\Notifications\Test; +use Livewire\Attributes\Validate; use Livewire\Component; class Discord extends Component { public Team $team; - protected $rules = [ - 'team.discord_enabled' => 'nullable|boolean', - 'team.discord_webhook_url' => 'required|url', - 'team.discord_notifications_test' => 'nullable|boolean', - 'team.discord_notifications_deployments' => 'nullable|boolean', - 'team.discord_notifications_status_changes' => 'nullable|boolean', - 'team.discord_notifications_database_backups' => 'nullable|boolean', - 'team.discord_notifications_scheduled_tasks' => 'nullable|boolean', - ]; + #[Validate(['boolean'])] + public bool $discordEnabled = false; - protected $validationAttributes = [ - 'team.discord_webhook_url' => 'Discord Webhook', - ]; + #[Validate(['url', 'nullable'])] + public ?string $discordWebhookUrl = null; + + #[Validate(['boolean'])] + public bool $discordNotificationsTest = false; + + #[Validate(['boolean'])] + public bool $discordNotificationsDeployments = false; + + #[Validate(['boolean'])] + public bool $discordNotificationsStatusChanges = false; + + #[Validate(['boolean'])] + public bool $discordNotificationsDatabaseBackups = false; + + #[Validate(['boolean'])] + public bool $discordNotificationsScheduledTasks = false; + + #[Validate(['boolean'])] + public bool $discordNotificationsServerDiskUsage = false; public function mount() { - $this->team = auth()->user()->currentTeam(); + try { + $this->team = auth()->user()->currentTeam(); + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->team->discord_enabled = $this->discordEnabled; + $this->team->discord_webhook_url = $this->discordWebhookUrl; + $this->team->discord_notifications_test = $this->discordNotificationsTest; + $this->team->discord_notifications_deployments = $this->discordNotificationsDeployments; + $this->team->discord_notifications_status_changes = $this->discordNotificationsStatusChanges; + $this->team->discord_notifications_database_backups = $this->discordNotificationsDatabaseBackups; + $this->team->discord_notifications_scheduled_tasks = $this->discordNotificationsScheduledTasks; + $this->team->discord_notifications_server_disk_usage = $this->discordNotificationsServerDiskUsage; + $this->team->save(); + refreshSession(); + } else { + $this->discordEnabled = $this->team->discord_enabled; + $this->discordWebhookUrl = $this->team->discord_webhook_url; + $this->discordNotificationsTest = $this->team->discord_notifications_test; + $this->discordNotificationsDeployments = $this->team->discord_notifications_deployments; + $this->discordNotificationsStatusChanges = $this->team->discord_notifications_status_changes; + $this->discordNotificationsDatabaseBackups = $this->team->discord_notifications_database_backups; + $this->discordNotificationsScheduledTasks = $this->team->discord_notifications_scheduled_tasks; + $this->discordNotificationsServerDiskUsage = $this->team->discord_notifications_server_disk_usage; + } + } + + public function instantSaveDiscordEnabled() + { + try { + $this->validate([ + 'discordWebhookUrl' => 'required', + ], [ + 'discordWebhookUrl.required' => 'Discord Webhook URL is required.', + ]); + $this->saveModel(); + } catch (\Throwable $e) { + $this->discordEnabled = false; + + return handleError($e, $this); + } } public function instantSave() { try { - $this->submit(); + $this->syncData(true); } catch (\Throwable $e) { - ray($e->getMessage()); - $this->team->discord_enabled = false; - $this->validate(); + return handleError($e, $this); } } public function submit() { - $this->resetErrorBag(); - $this->validate(); - $this->saveModel(); + try { + $this->resetErrorBag(); + $this->syncData(true); + $this->saveModel(); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function saveModel() { - $this->team->save(); + $this->syncData(true); refreshSession(); $this->dispatch('success', 'Settings saved.'); } public function sendTestNotification() { - $this->team?->notify(new Test()); - $this->dispatch('success', 'Test notification sent.'); + try { + $this->team->notify(new Test); + $this->dispatch('success', 'Test notification sent.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function render() diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index 91c108edc..56f07f3a9 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -2,118 +2,199 @@ namespace App\Livewire\Notifications; -use App\Models\InstanceSettings; use App\Models\Team; use App\Notifications\Test; +use Illuminate\Support\Facades\RateLimiter; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class Email extends Component { public Team $team; + #[Locked] public string $emails; - public bool $sharedEmailEnabled = false; + #[Validate(['boolean'])] + public bool $smtpEnabled = false; - protected $rules = [ - 'team.smtp_enabled' => 'nullable|boolean', - 'team.smtp_from_address' => 'required|email', - 'team.smtp_from_name' => 'required', - 'team.smtp_recipients' => 'nullable', - 'team.smtp_host' => 'required', - 'team.smtp_port' => 'required', - 'team.smtp_encryption' => 'nullable', - 'team.smtp_username' => 'nullable', - 'team.smtp_password' => 'nullable', - 'team.smtp_timeout' => 'nullable', - 'team.smtp_notifications_test' => 'nullable|boolean', - 'team.smtp_notifications_deployments' => 'nullable|boolean', - 'team.smtp_notifications_status_changes' => 'nullable|boolean', - 'team.smtp_notifications_database_backups' => 'nullable|boolean', - 'team.smtp_notifications_scheduled_tasks' => 'nullable|boolean', - 'team.use_instance_email_settings' => 'boolean', - 'team.resend_enabled' => 'nullable|boolean', - 'team.resend_api_key' => 'nullable', - ]; + #[Validate(['boolean'])] + public bool $useInstanceEmailSettings = false; - protected $validationAttributes = [ - 'team.smtp_from_address' => 'From Address', - 'team.smtp_from_name' => 'From Name', - 'team.smtp_recipients' => 'Recipients', - 'team.smtp_host' => 'Host', - 'team.smtp_port' => 'Port', - 'team.smtp_encryption' => 'Encryption', - 'team.smtp_username' => 'Username', - 'team.smtp_password' => 'Password', - 'team.smtp_timeout' => 'Timeout', - 'team.resend_enabled' => 'Resend Enabled', - 'team.resend_api_key' => 'Resend API Key', - ]; + #[Validate(['nullable', 'email'])] + public ?string $smtpFromAddress = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpFromName = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpRecipients = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpHost = null; + + #[Validate(['nullable', 'numeric'])] + public ?int $smtpPort = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpEncryption = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpUsername = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpPassword = null; + + #[Validate(['nullable', 'numeric'])] + public ?int $smtpTimeout = null; + + #[Validate(['boolean'])] + public bool $smtpNotificationsTest = false; + + #[Validate(['boolean'])] + public bool $smtpNotificationsDeployments = false; + + #[Validate(['boolean'])] + public bool $smtpNotificationsStatusChanges = false; + + #[Validate(['boolean'])] + public bool $smtpNotificationsDatabaseBackups = false; + + #[Validate(['boolean'])] + public bool $smtpNotificationsScheduledTasks = false; + + #[Validate(['boolean'])] + public bool $smtpNotificationsServerDiskUsage = false; + + #[Validate(['boolean'])] + public bool $resendEnabled; + + #[Validate(['nullable', 'string'])] + public ?string $resendApiKey = null; public function mount() - { - $this->team = auth()->user()->currentTeam(); - ['sharedEmailEnabled' => $this->sharedEmailEnabled] = $this->team->limits; - $this->emails = auth()->user()->email; - } - - public function submitFromFields() { try { - $this->resetErrorBag(); - $this->validate([ - 'team.smtp_from_address' => 'required|email', - 'team.smtp_from_name' => 'required', - ]); - $this->team->save(); - refreshSession(); - $this->dispatch('success', 'Settings saved.'); + $this->team = auth()->user()->currentTeam(); + $this->emails = auth()->user()->email; + $this->syncData(); } catch (\Throwable $e) { return handleError($e, $this); } } + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->team->smtp_enabled = $this->smtpEnabled; + $this->team->smtp_from_address = $this->smtpFromAddress; + $this->team->smtp_from_name = $this->smtpFromName; + $this->team->smtp_host = $this->smtpHost; + $this->team->smtp_port = $this->smtpPort; + $this->team->smtp_encryption = $this->smtpEncryption; + $this->team->smtp_username = $this->smtpUsername; + $this->team->smtp_password = $this->smtpPassword; + $this->team->smtp_timeout = $this->smtpTimeout; + $this->team->smtp_recipients = $this->smtpRecipients; + $this->team->smtp_notifications_test = $this->smtpNotificationsTest; + $this->team->smtp_notifications_deployments = $this->smtpNotificationsDeployments; + $this->team->smtp_notifications_status_changes = $this->smtpNotificationsStatusChanges; + $this->team->smtp_notifications_database_backups = $this->smtpNotificationsDatabaseBackups; + $this->team->smtp_notifications_scheduled_tasks = $this->smtpNotificationsScheduledTasks; + $this->team->smtp_notifications_server_disk_usage = $this->smtpNotificationsServerDiskUsage; + $this->team->use_instance_email_settings = $this->useInstanceEmailSettings; + $this->team->resend_enabled = $this->resendEnabled; + $this->team->resend_api_key = $this->resendApiKey; + $this->team->save(); + refreshSession(); + } else { + $this->smtpEnabled = $this->team->smtp_enabled; + $this->smtpFromAddress = $this->team->smtp_from_address; + $this->smtpFromName = $this->team->smtp_from_name; + $this->smtpHost = $this->team->smtp_host; + $this->smtpPort = $this->team->smtp_port; + $this->smtpEncryption = $this->team->smtp_encryption; + $this->smtpUsername = $this->team->smtp_username; + $this->smtpPassword = $this->team->smtp_password; + $this->smtpTimeout = $this->team->smtp_timeout; + $this->smtpRecipients = $this->team->smtp_recipients; + $this->smtpNotificationsTest = $this->team->smtp_notifications_test; + $this->smtpNotificationsDeployments = $this->team->smtp_notifications_deployments; + $this->smtpNotificationsStatusChanges = $this->team->smtp_notifications_status_changes; + $this->smtpNotificationsDatabaseBackups = $this->team->smtp_notifications_database_backups; + $this->smtpNotificationsScheduledTasks = $this->team->smtp_notifications_scheduled_tasks; + $this->smtpNotificationsServerDiskUsage = $this->team->smtp_notifications_server_disk_usage; + $this->useInstanceEmailSettings = $this->team->use_instance_email_settings; + $this->resendEnabled = $this->team->resend_enabled; + $this->resendApiKey = $this->team->resend_api_key; + } + } + public function sendTestNotification() { - $this->team?->notify(new Test($this->emails)); - $this->dispatch('success', 'Test Email sent.'); + try { + $executed = RateLimiter::attempt( + 'test-email:'.$this->team->id, + $perMinute = 0, + function () { + $this->team?->notify(new Test($this->emails)); + $this->dispatch('success', 'Test Email sent.'); + }, + $decaySeconds = 10, + ); + + if (! $executed) { + throw new \Exception('Too many messages sent!'); + } + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function instantSaveInstance() { try { - if (! $this->sharedEmailEnabled) { - throw new \Exception('Not allowed to change settings. Please upgrade your subscription.'); - } - $this->team->smtp_enabled = false; - $this->team->resend_enabled = false; - $this->team->save(); - refreshSession(); - $this->dispatch('success', 'Settings saved.'); + $this->smtpEnabled = false; + $this->resendEnabled = false; + $this->saveModel(); } catch (\Throwable $e) { return handleError($e, $this); } } + public function instantSaveSmtpEnabled() + { + try { + $this->validate([ + 'smtpHost' => 'required', + 'smtpPort' => 'required|numeric', + ], [ + 'smtpHost.required' => 'SMTP Host is required.', + 'smtpPort.required' => 'SMTP Port is required.', + ]); + $this->resendEnabled = false; + $this->saveModel(); + } catch (\Throwable $e) { + $this->smtpEnabled = false; + + return handleError($e, $this); + } + } + public function instantSaveResend() { try { - $this->team->smtp_enabled = false; - $this->submitResend(); + $this->validate([ + 'resendApiKey' => 'required', + ], [ + 'resendApiKey.required' => 'Resend API Key is required.', + ]); + $this->smtpEnabled = false; + $this->saveModel(); } catch (\Throwable $e) { - $this->team->smtp_enabled = false; - - return handleError($e, $this); - } - } - - public function instantSave() - { - try { - $this->team->resend_enabled = false; - $this->submit(); - } catch (\Throwable $e) { - $this->team->smtp_enabled = false; + $this->resendEnabled = false; return handleError($e, $this); } @@ -121,7 +202,7 @@ class Email extends Component public function saveModel() { - $this->team->save(); + $this->syncData(true); refreshSession(); $this->dispatch('success', 'Settings saved.'); } @@ -130,79 +211,37 @@ class Email extends Component { try { $this->resetErrorBag(); - if (! $this->team->use_instance_email_settings) { - $this->validate([ - 'team.smtp_from_address' => 'required|email', - 'team.smtp_from_name' => 'required', - 'team.smtp_host' => 'required', - 'team.smtp_port' => 'required|numeric', - 'team.smtp_encryption' => 'nullable', - 'team.smtp_username' => 'nullable', - 'team.smtp_password' => 'nullable', - 'team.smtp_timeout' => 'nullable', - ]); - } - $this->team->save(); - refreshSession(); - $this->dispatch('success', 'Settings saved.'); + $this->saveModel(); } catch (\Throwable $e) { - $this->team->smtp_enabled = false; - - return handleError($e, $this); - } - } - - public function submitResend() - { - try { - $this->resetErrorBag(); - $this->validate([ - 'team.smtp_from_address' => 'required|email', - 'team.smtp_from_name' => 'required', - 'team.resend_api_key' => 'required', - ]); - $this->team->save(); - refreshSession(); - $this->dispatch('success', 'Settings saved.'); - } catch (\Throwable $e) { - $this->team->resend_enabled = false; - return handleError($e, $this); } } public function copyFromInstanceSettings() { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); + if ($settings->smtp_enabled) { - $team = currentTeam(); - $team->update([ - 'smtp_enabled' => $settings->smtp_enabled, - 'smtp_from_address' => $settings->smtp_from_address, - 'smtp_from_name' => $settings->smtp_from_name, - 'smtp_recipients' => $settings->smtp_recipients, - 'smtp_host' => $settings->smtp_host, - 'smtp_port' => $settings->smtp_port, - 'smtp_encryption' => $settings->smtp_encryption, - 'smtp_username' => $settings->smtp_username, - 'smtp_password' => $settings->smtp_password, - 'smtp_timeout' => $settings->smtp_timeout, - ]); - refreshSession(); - $this->team = $team; - $this->dispatch('success', 'Settings saved.'); + $this->smtpEnabled = true; + $this->smtpFromAddress = $settings->smtp_from_address; + $this->smtpFromName = $settings->smtp_from_name; + $this->smtpRecipients = $settings->smtp_recipients; + $this->smtpHost = $settings->smtp_host; + $this->smtpPort = $settings->smtp_port; + $this->smtpEncryption = $settings->smtp_encryption; + $this->smtpUsername = $settings->smtp_username; + $this->smtpPassword = $settings->smtp_password; + $this->smtpTimeout = $settings->smtp_timeout; + $this->resendEnabled = false; + $this->saveModel(); return; } if ($settings->resend_enabled) { - $team = currentTeam(); - $team->update([ - 'resend_enabled' => $settings->resend_enabled, - 'resend_api_key' => $settings->resend_api_key, - ]); - refreshSession(); - $this->team = $team; - $this->dispatch('success', 'Settings saved.'); + $this->resendEnabled = true; + $this->resendApiKey = $settings->resend_api_key; + $this->smtpEnabled = false; + $this->saveModel(); return; } diff --git a/app/Livewire/Notifications/Telegram.php b/app/Livewire/Notifications/Telegram.php index 16123f123..15ec20577 100644 --- a/app/Livewire/Notifications/Telegram.php +++ b/app/Livewire/Notifications/Telegram.php @@ -4,67 +4,157 @@ namespace App\Livewire\Notifications; use App\Models\Team; use App\Notifications\Test; +use Livewire\Attributes\Validate; use Livewire\Component; class Telegram extends Component { public Team $team; - protected $rules = [ - 'team.telegram_enabled' => 'nullable|boolean', - 'team.telegram_token' => 'required|string', - 'team.telegram_chat_id' => 'required|string', - 'team.telegram_notifications_test' => 'nullable|boolean', - 'team.telegram_notifications_deployments' => 'nullable|boolean', - 'team.telegram_notifications_status_changes' => 'nullable|boolean', - 'team.telegram_notifications_database_backups' => 'nullable|boolean', - 'team.telegram_notifications_scheduled_tasks' => 'nullable|boolean', - 'team.telegram_notifications_test_message_thread_id' => 'nullable|string', - 'team.telegram_notifications_deployments_message_thread_id' => 'nullable|string', - 'team.telegram_notifications_status_changes_message_thread_id' => 'nullable|string', - 'team.telegram_notifications_database_backups_message_thread_id' => 'nullable|string', - 'team.telegram_notifications_scheduled_tasks_thread_id' => 'nullable|string', - ]; + #[Validate(['boolean'])] + public bool $telegramEnabled = false; - protected $validationAttributes = [ - 'team.telegram_token' => 'Token', - 'team.telegram_chat_id' => 'Chat ID', - ]; + #[Validate(['nullable', 'string'])] + public ?string $telegramToken = null; + + #[Validate(['nullable', 'string'])] + public ?string $telegramChatId = null; + + #[Validate(['boolean'])] + public bool $telegramNotificationsTest = false; + + #[Validate(['boolean'])] + public bool $telegramNotificationsDeployments = false; + + #[Validate(['boolean'])] + public bool $telegramNotificationsStatusChanges = false; + + #[Validate(['boolean'])] + public bool $telegramNotificationsDatabaseBackups = false; + + #[Validate(['boolean'])] + public bool $telegramNotificationsScheduledTasks = false; + + #[Validate(['nullable', 'string'])] + public ?string $telegramNotificationsTestMessageThreadId = null; + + #[Validate(['nullable', 'string'])] + public ?string $telegramNotificationsDeploymentsMessageThreadId = null; + + #[Validate(['nullable', 'string'])] + public ?string $telegramNotificationsStatusChangesMessageThreadId = null; + + #[Validate(['nullable', 'string'])] + public ?string $telegramNotificationsDatabaseBackupsMessageThreadId = null; + + #[Validate(['nullable', 'string'])] + public ?string $telegramNotificationsScheduledTasksThreadId = null; + + #[Validate(['boolean'])] + public bool $telegramNotificationsServerDiskUsage = false; public function mount() { - $this->team = auth()->user()->currentTeam(); + try { + $this->team = auth()->user()->currentTeam(); + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->team->telegram_enabled = $this->telegramEnabled; + $this->team->telegram_token = $this->telegramToken; + $this->team->telegram_chat_id = $this->telegramChatId; + $this->team->telegram_notifications_test = $this->telegramNotificationsTest; + $this->team->telegram_notifications_deployments = $this->telegramNotificationsDeployments; + $this->team->telegram_notifications_status_changes = $this->telegramNotificationsStatusChanges; + $this->team->telegram_notifications_database_backups = $this->telegramNotificationsDatabaseBackups; + $this->team->telegram_notifications_scheduled_tasks = $this->telegramNotificationsScheduledTasks; + $this->team->telegram_notifications_test_message_thread_id = $this->telegramNotificationsTestMessageThreadId; + $this->team->telegram_notifications_deployments_message_thread_id = $this->telegramNotificationsDeploymentsMessageThreadId; + $this->team->telegram_notifications_status_changes_message_thread_id = $this->telegramNotificationsStatusChangesMessageThreadId; + $this->team->telegram_notifications_database_backups_message_thread_id = $this->telegramNotificationsDatabaseBackupsMessageThreadId; + $this->team->telegram_notifications_scheduled_tasks_thread_id = $this->telegramNotificationsScheduledTasksThreadId; + $this->team->telegram_notifications_server_disk_usage = $this->telegramNotificationsServerDiskUsage; + $this->team->save(); + refreshSession(); + } else { + $this->telegramEnabled = $this->team->telegram_enabled; + $this->telegramToken = $this->team->telegram_token; + $this->telegramChatId = $this->team->telegram_chat_id; + $this->telegramNotificationsTest = $this->team->telegram_notifications_test; + $this->telegramNotificationsDeployments = $this->team->telegram_notifications_deployments; + $this->telegramNotificationsStatusChanges = $this->team->telegram_notifications_status_changes; + $this->telegramNotificationsDatabaseBackups = $this->team->telegram_notifications_database_backups; + $this->telegramNotificationsScheduledTasks = $this->team->telegram_notifications_scheduled_tasks; + $this->telegramNotificationsTestMessageThreadId = $this->team->telegram_notifications_test_message_thread_id; + $this->telegramNotificationsDeploymentsMessageThreadId = $this->team->telegram_notifications_deployments_message_thread_id; + $this->telegramNotificationsStatusChangesMessageThreadId = $this->team->telegram_notifications_status_changes_message_thread_id; + $this->telegramNotificationsDatabaseBackupsMessageThreadId = $this->team->telegram_notifications_database_backups_message_thread_id; + $this->telegramNotificationsScheduledTasksThreadId = $this->team->telegram_notifications_scheduled_tasks_thread_id; + $this->telegramNotificationsServerDiskUsage = $this->team->telegram_notifications_server_disk_usage; + } + } public function instantSave() { try { - $this->submit(); + $this->syncData(true); } catch (\Throwable $e) { - ray($e->getMessage()); - $this->team->telegram_enabled = false; - $this->validate(); + return handleError($e, $this); } } public function submit() { - $this->resetErrorBag(); - $this->validate(); - $this->saveModel(); + try { + $this->resetErrorBag(); + $this->syncData(true); + $this->saveModel(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function instantSaveTelegramEnabled() + { + try { + $this->validate([ + 'telegramToken' => 'required', + 'telegramChatId' => 'required', + ], [ + 'telegramToken.required' => 'Telegram Token is required.', + 'telegramChatId.required' => 'Telegram Chat ID is required.', + ]); + $this->saveModel(); + } catch (\Throwable $e) { + $this->telegramEnabled = false; + + return handleError($e, $this); + } } public function saveModel() { - $this->team->save(); + $this->syncData(true); refreshSession(); $this->dispatch('success', 'Settings saved.'); } public function sendTestNotification() { - $this->team?->notify(new Test()); - $this->dispatch('success', 'Test notification sent.'); + try { + $this->team->notify(new Test); + $this->dispatch('success', 'Test notification sent.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function render() diff --git a/app/Livewire/Profile/Index.php b/app/Livewire/Profile/Index.php index 3be1b05ce..53314cd5c 100644 --- a/app/Livewire/Profile/Index.php +++ b/app/Livewire/Profile/Index.php @@ -2,7 +2,9 @@ namespace App\Livewire\Profile; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; +use Illuminate\Validation\Rules\Password; use Livewire\Attributes\Validate; use Livewire\Component; @@ -23,9 +25,9 @@ class Index extends Component public function mount() { - $this->userId = auth()->user()->id; - $this->name = auth()->user()->name; - $this->email = auth()->user()->email; + $this->userId = Auth::id(); + $this->name = Auth::user()->name; + $this->email = Auth::user()->email; } public function submit() @@ -34,7 +36,7 @@ class Index extends Component $this->validate([ 'name' => 'required', ]); - auth()->user()->update([ + Auth::user()->update([ 'name' => $this->name, ]); @@ -48,9 +50,8 @@ class Index extends Component { try { $this->validate([ - 'current_password' => 'required', - 'new_password' => 'required|min:8', - 'new_password_confirmation' => 'required|min:8|same:new_password', + 'current_password' => ['required'], + 'new_password' => ['required', Password::defaults(), 'confirmed'], ]); if (! Hash::check($this->current_password, auth()->user()->password)) { $this->dispatch('error', 'Current password is incorrect.'); diff --git a/app/Livewire/Project/AddEmpty.php b/app/Livewire/Project/AddEmpty.php index c3353be84..fd976548a 100644 --- a/app/Livewire/Project/AddEmpty.php +++ b/app/Livewire/Project/AddEmpty.php @@ -3,24 +3,17 @@ namespace App\Livewire\Project; use App\Models\Project; +use Livewire\Attributes\Validate; use Livewire\Component; class AddEmpty extends Component { - public string $name = ''; + #[Validate(['required', 'string', 'min:3'])] + public string $name; + #[Validate(['nullable', 'string'])] public string $description = ''; - protected $rules = [ - 'name' => 'required|string|min:3', - 'description' => 'nullable|string', - ]; - - protected $validationAttributes = [ - 'name' => 'Project Name', - 'description' => 'Project Description', - ]; - public function submit() { try { @@ -34,8 +27,6 @@ class AddEmpty extends Component return redirect()->route('project.show', $project->uuid); } catch (\Throwable $e) { return handleError($e, $this); - } finally { - $this->name = ''; } } } diff --git a/app/Livewire/Project/AddEnvironment.php b/app/Livewire/Project/AddEnvironment.php deleted file mode 100644 index 7b2767dc6..000000000 --- a/app/Livewire/Project/AddEnvironment.php +++ /dev/null @@ -1,44 +0,0 @@ - 'required|string|min:3', - ]; - - protected $validationAttributes = [ - 'name' => 'Environment Name', - ]; - - public function submit() - { - try { - $this->validate(); - $environment = Environment::create([ - 'name' => $this->name, - 'project_id' => $this->project->id, - ]); - - return redirect()->route('project.resource.index', [ - 'project_uuid' => $this->project->uuid, - 'environment_name' => $environment->name, - ]); - } catch (\Throwable $e) { - handleError($e, $this); - } finally { - $this->name = ''; - } - } -} diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php index 3b402b3ec..05ac25429 100644 --- a/app/Livewire/Project/Application/Advanced.php +++ b/app/Livewire/Project/Application/Advanced.php @@ -3,100 +3,200 @@ namespace App\Livewire\Project\Application; use App\Models\Application; +use Livewire\Attributes\Validate; use Livewire\Component; class Advanced extends Component { public Application $application; - public bool $is_force_https_enabled; + #[Validate(['boolean'])] + public bool $isForceHttpsEnabled = false; - public bool $is_gzip_enabled; + #[Validate(['boolean'])] + public bool $isGitSubmodulesEnabled = false; - public bool $is_stripprefix_enabled; + #[Validate(['boolean'])] + public bool $isGitLfsEnabled = false; - protected $rules = [ - 'application.settings.is_git_submodules_enabled' => 'boolean|required', - 'application.settings.is_git_lfs_enabled' => 'boolean|required', - 'application.settings.is_preview_deployments_enabled' => 'boolean|required', - 'application.settings.is_auto_deploy_enabled' => 'boolean|required', - 'is_force_https_enabled' => 'boolean|required', - 'application.settings.is_log_drain_enabled' => 'boolean|required', - 'application.settings.is_gpu_enabled' => 'boolean|required', - 'application.settings.is_build_server_enabled' => 'boolean|required', - 'application.settings.is_consistent_container_name_enabled' => 'boolean|required', - 'application.settings.custom_internal_name' => 'string|nullable', - 'application.settings.is_gzip_enabled' => 'boolean|required', - 'application.settings.is_stripprefix_enabled' => 'boolean|required', - 'application.settings.gpu_driver' => 'string|required', - 'application.settings.gpu_count' => 'string|required', - 'application.settings.gpu_device_ids' => 'string|required', - 'application.settings.gpu_options' => 'string|required', - 'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required', - 'application.settings.connect_to_docker_network' => 'boolean|required', - ]; + #[Validate(['boolean'])] + public bool $isPreviewDeploymentsEnabled = false; + + #[Validate(['boolean'])] + public bool $isAutoDeployEnabled = true; + + #[Validate(['boolean'])] + public bool $isLogDrainEnabled = false; + + #[Validate(['boolean'])] + public bool $isGpuEnabled = false; + + #[Validate(['string'])] + public string $gpuDriver = ''; + + #[Validate(['string', 'nullable'])] + public ?string $gpuCount = null; + + #[Validate(['string', 'nullable'])] + public ?string $gpuDeviceIds = null; + + #[Validate(['string', 'nullable'])] + public ?string $gpuOptions = null; + + #[Validate(['boolean'])] + public bool $isBuildServerEnabled = false; + + #[Validate(['boolean'])] + public bool $isConsistentContainerNameEnabled = false; + + #[Validate(['string', 'nullable'])] + public ?string $customInternalName = null; + + #[Validate(['boolean'])] + public bool $isGzipEnabled = true; + + #[Validate(['boolean'])] + public bool $isStripprefixEnabled = true; + + #[Validate(['boolean'])] + public bool $isRawComposeDeploymentEnabled = false; + + #[Validate(['boolean'])] + public bool $isConnectToDockerNetworkEnabled = false; public function mount() { - $this->is_force_https_enabled = $this->application->isForceHttpsEnabled(); - $this->is_gzip_enabled = $this->application->isGzipEnabled(); - $this->is_stripprefix_enabled = $this->application->isStripprefixEnabled(); + try { + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->application->settings->is_force_https_enabled = $this->isForceHttpsEnabled; + $this->application->settings->is_git_submodules_enabled = $this->isGitSubmodulesEnabled; + $this->application->settings->is_git_lfs_enabled = $this->isGitLfsEnabled; + $this->application->settings->is_preview_deployments_enabled = $this->isPreviewDeploymentsEnabled; + $this->application->settings->is_auto_deploy_enabled = $this->isAutoDeployEnabled; + $this->application->settings->is_log_drain_enabled = $this->isLogDrainEnabled; + $this->application->settings->is_gpu_enabled = $this->isGpuEnabled; + $this->application->settings->gpu_driver = $this->gpuDriver; + $this->application->settings->gpu_count = $this->gpuCount; + $this->application->settings->gpu_device_ids = $this->gpuDeviceIds; + $this->application->settings->gpu_options = $this->gpuOptions; + $this->application->settings->is_build_server_enabled = $this->isBuildServerEnabled; + $this->application->settings->is_consistent_container_name_enabled = $this->isConsistentContainerNameEnabled; + $this->application->settings->custom_internal_name = $this->customInternalName; + $this->application->settings->is_gzip_enabled = $this->isGzipEnabled; + $this->application->settings->is_stripprefix_enabled = $this->isStripprefixEnabled; + $this->application->settings->is_raw_compose_deployment_enabled = $this->isRawComposeDeploymentEnabled; + $this->application->settings->connect_to_docker_network = $this->isConnectToDockerNetworkEnabled; + $this->application->settings->save(); + } else { + $this->isForceHttpsEnabled = $this->application->isForceHttpsEnabled(); + $this->isGzipEnabled = $this->application->isGzipEnabled(); + $this->isStripprefixEnabled = $this->application->isStripprefixEnabled(); + $this->isLogDrainEnabled = $this->application->isLogDrainEnabled(); + + $this->isGitSubmodulesEnabled = $this->application->settings->is_git_submodules_enabled; + $this->isGitLfsEnabled = $this->application->settings->is_git_lfs_enabled; + $this->isPreviewDeploymentsEnabled = $this->application->settings->is_preview_deployments_enabled; + $this->isAutoDeployEnabled = $this->application->settings->is_auto_deploy_enabled; + $this->isGpuEnabled = $this->application->settings->is_gpu_enabled; + $this->gpuDriver = $this->application->settings->gpu_driver; + $this->gpuCount = $this->application->settings->gpu_count; + $this->gpuDeviceIds = $this->application->settings->gpu_device_ids; + $this->gpuOptions = $this->application->settings->gpu_options; + $this->isBuildServerEnabled = $this->application->settings->is_build_server_enabled; + $this->isConsistentContainerNameEnabled = $this->application->settings->is_consistent_container_name_enabled; + $this->customInternalName = $this->application->settings->custom_internal_name; + $this->isRawComposeDeploymentEnabled = $this->application->settings->is_raw_compose_deployment_enabled; + $this->isConnectToDockerNetworkEnabled = $this->application->settings->connect_to_docker_network; + } } public function instantSave() { - if ($this->application->isLogDrainEnabled()) { - if (! $this->application->destination->server->isLogDrainEnabled()) { - $this->application->settings->is_log_drain_enabled = false; - $this->dispatch('error', 'Log drain is not enabled on this server.'); + try { + if ($this->isLogDrainEnabled) { + if (! $this->application->destination->server->isLogDrainEnabled()) { + $this->isLogDrainEnabled = false; + $this->syncData(true); + $this->dispatch('error', 'Log drain is not enabled on this server.'); - return; + return; + } } + if ($this->application->isForceHttpsEnabled() !== $this->isForceHttpsEnabled || + $this->application->isGzipEnabled() !== $this->isGzipEnabled || + $this->application->isStripprefixEnabled() !== $this->isStripprefixEnabled + ) { + $this->dispatch('resetDefaultLabels', false); + } + + if ($this->application->settings->is_raw_compose_deployment_enabled) { + $this->application->oldRawParser(); + } else { + $this->application->parse(); + } + $this->syncData(true); + $this->dispatch('success', 'Settings saved.'); + $this->dispatch('configurationChanged'); + } catch (\Throwable $e) { + return handleError($e, $this); } - if ($this->application->settings->is_force_https_enabled !== $this->is_force_https_enabled) { - $this->application->settings->is_force_https_enabled = $this->is_force_https_enabled; - $this->dispatch('resetDefaultLabels', false); - } - if ($this->application->settings->is_gzip_enabled !== $this->is_gzip_enabled) { - $this->application->settings->is_gzip_enabled = $this->is_gzip_enabled; - $this->dispatch('resetDefaultLabels', false); - } - if ($this->application->settings->is_stripprefix_enabled !== $this->is_stripprefix_enabled) { - $this->application->settings->is_stripprefix_enabled = $this->is_stripprefix_enabled; - $this->dispatch('resetDefaultLabels', false); - } - if ($this->application->settings->is_raw_compose_deployment_enabled) { - $this->application->parseRawCompose(); - } else { - $this->application->parseCompose(); - } - $this->application->settings->save(); - $this->dispatch('success', 'Settings saved.'); - $this->dispatch('configurationChanged'); } public function submit() { - if ($this->application->settings->gpu_count && $this->application->settings->gpu_device_ids) { - $this->dispatch('error', 'You cannot set both GPU count and GPU device IDs.'); - $this->application->settings->gpu_count = null; - $this->application->settings->gpu_device_ids = null; - $this->application->settings->save(); + try { + if ($this->gpuCount && $this->gpuDeviceIds) { + $this->dispatch('error', 'You cannot set both GPU count and GPU device IDs.'); + $this->gpuCount = null; + $this->gpuDeviceIds = null; + $this->syncData(true); - return; + return; + } + $this->syncData(true); + $this->dispatch('success', 'Settings saved.'); + } catch (\Throwable $e) { + return handleError($e, $this); } - $this->application->settings->save(); - $this->dispatch('success', 'Settings saved.'); } public function saveCustomName() { - if (isset($this->application->settings->custom_internal_name)) { - $this->application->settings->custom_internal_name = str($this->application->settings->custom_internal_name)->slug()->value(); + if (str($this->customInternalName)->isNotEmpty()) { + $this->customInternalName = str($this->customInternalName)->slug()->value(); } else { - $this->application->settings->custom_internal_name = null; + $this->customInternalName = null; } - $this->application->settings->save(); + if (is_null($this->customInternalName)) { + $this->syncData(true); + $this->dispatch('success', 'Custom name saved.'); + + return; + } + $customInternalName = $this->customInternalName; + $server = $this->application->destination->server; + $allApplications = $server->applications(); + + $foundSameInternalName = $allApplications->filter(function ($application) { + return $application->id !== $this->application->id && $application->settings->custom_internal_name === $this->customInternalName; + }); + if ($foundSameInternalName->isNotEmpty()) { + $this->dispatch('error', 'This custom container name is already in use by another application on this server.'); + $this->customInternalName = $customInternalName; + $this->syncData(true); + + return; + } + $this->syncData(true); $this->dispatch('success', 'Custom name saved.'); } diff --git a/app/Livewire/Project/Application/Deployment/Show.php b/app/Livewire/Project/Application/Deployment/Show.php index 84a24255c..04170fa28 100644 --- a/app/Livewire/Project/Application/Deployment/Show.php +++ b/app/Livewire/Project/Application/Deployment/Show.php @@ -64,11 +64,25 @@ class Show extends Component { $this->dispatch('deploymentFinished'); $this->application_deployment_queue->refresh(); - if (data_get($this->application_deployment_queue, 'status') == 'finished' || data_get($this->application_deployment_queue, 'status') == 'failed') { + if (data_get($this->application_deployment_queue, 'status') === 'finished' || data_get($this->application_deployment_queue, 'status') === 'failed') { $this->isKeepAliveOn = false; } } + public function getLogLinesProperty() + { + return decode_remote_command_output($this->application_deployment_queue)->map(function ($logLine) { + $logLine['line'] = e($logLine['line']); + $logLine['line'] = preg_replace( + '/(https?:\/\/[^\s]+)/', + '$1', + $logLine['line'], + ); + + return $logLine; + }); + } + public function render() { return view('livewire.project.application.deployment.show'); diff --git a/app/Livewire/Project/Application/DeploymentNavbar.php b/app/Livewire/Project/Application/DeploymentNavbar.php index cbbe98d99..6a6fa2482 100644 --- a/app/Livewire/Project/Application/DeploymentNavbar.php +++ b/app/Livewire/Project/Application/DeploymentNavbar.php @@ -46,18 +46,21 @@ class DeploymentNavbar extends Component try { force_start_deployment($this->application_deployment_queue); } catch (\Throwable $e) { - ray($e); - return handleError($e, $this); } } public function cancel() { + $kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}"; + $build_server_id = $this->application_deployment_queue->build_server_id; + $server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id; try { - $kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}"; - $server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id; - $server = Server::find($server_id); + if ($this->application->settings->is_build_server_enabled) { + $server = Server::find($build_server_id); + } else { + $server = Server::find($server_id); + } if ($this->application_deployment_queue->logs) { $previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR); @@ -76,14 +79,13 @@ class DeploymentNavbar extends Component } instant_remote_process([$kill_command], $server); } catch (\Throwable $e) { - ray($e); - return handleError($e, $this); } finally { $this->application_deployment_queue->update([ 'current_process_id' => null, 'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value, ]); + next_after_cancel($server); } } } diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 60cdee48e..ff29b74e9 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -2,11 +2,11 @@ namespace App\Livewire\Project\Application; +use App\Actions\Application\GenerateConfig; use App\Models\Application; -use App\Models\LocalFileVolume; use Illuminate\Support\Collection; -use Illuminate\Support\Str; use Livewire\Component; +use Spatie\Url\Url; use Visus\Cuid2\Cuid2; class General extends Component @@ -31,6 +31,8 @@ class General extends Component public ?string $ports_exposes = null; + public bool $is_preserve_repository_enabled = false; + public bool $is_container_label_escape_enabled = true; public $customLabels; @@ -41,8 +43,6 @@ class General extends Component public ?string $initialDockerComposeLocation = null; - public ?string $initialDockerComposePrLocation = null; - public ?Collection $parsedServices; public $parsedServiceDomains = []; @@ -73,11 +73,8 @@ class General extends Component 'application.docker_registry_image_tag' => 'nullable', 'application.dockerfile_location' => 'nullable', 'application.docker_compose_location' => 'nullable', - 'application.docker_compose_pr_location' => 'nullable', 'application.docker_compose' => 'nullable', - 'application.docker_compose_pr' => 'nullable', 'application.docker_compose_raw' => 'nullable', - 'application.docker_compose_pr_raw' => 'nullable', 'application.dockerfile_target_build' => 'nullable', 'application.docker_compose_custom_start_command' => 'nullable', 'application.docker_compose_custom_build_command' => 'nullable', @@ -87,9 +84,12 @@ class General extends Component 'application.pre_deployment_command_container' => 'nullable', 'application.post_deployment_command' => 'nullable', 'application.post_deployment_command_container' => 'nullable', + 'application.custom_nginx_configuration' => 'nullable', 'application.settings.is_static' => 'boolean|required', 'application.settings.is_build_server_enabled' => 'boolean|required', 'application.settings.is_container_label_escape_enabled' => 'boolean|required', + 'application.settings.is_container_label_readonly_enabled' => 'boolean|required', + 'application.settings.is_preserve_repository_enabled' => 'boolean|required', 'application.watch_paths' => 'nullable', 'application.redirect' => 'string|required', ]; @@ -115,19 +115,19 @@ class General extends Component 'application.docker_registry_image_tag' => 'Docker registry image tag', 'application.dockerfile_location' => 'Dockerfile location', 'application.docker_compose_location' => 'Docker compose location', - 'application.docker_compose_pr_location' => 'Docker compose location', 'application.docker_compose' => 'Docker compose', - 'application.docker_compose_pr' => 'Docker compose', 'application.docker_compose_raw' => 'Docker compose raw', - 'application.docker_compose_pr_raw' => 'Docker compose raw', 'application.custom_labels' => 'Custom labels', 'application.dockerfile_target_build' => 'Dockerfile target build', 'application.custom_docker_run_options' => 'Custom docker run commands', 'application.docker_compose_custom_start_command' => 'Docker compose custom start command', 'application.docker_compose_custom_build_command' => 'Docker compose custom build command', + 'application.custom_nginx_configuration' => 'Custom Nginx configuration', 'application.settings.is_static' => 'Is static', 'application.settings.is_build_server_enabled' => 'Is build server enabled', 'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled', + 'application.settings.is_container_label_readonly_enabled' => 'Is container label readonly', + 'application.settings.is_preserve_repository_enabled' => 'Is preserve repository enabled', 'application.watch_paths' => 'Watch paths', 'application.redirect' => 'Redirect', ]; @@ -135,7 +135,7 @@ class General extends Component public function mount() { try { - $this->parsedServices = $this->application->parseCompose(); + $this->parsedServices = $this->application->parse(); if (is_null($this->parsedServices) || empty($this->parsedServices)) { $this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.'); @@ -150,9 +150,10 @@ class General extends Component } $this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : []; $this->ports_exposes = $this->application->ports_exposes; + $this->is_preserve_repository_enabled = $this->application->settings->is_preserve_repository_enabled; $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled; $this->customLabels = $this->application->parseContainerLabels(); - if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') { + if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) { $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); @@ -173,9 +174,19 @@ class General extends Component $this->application->settings->save(); $this->dispatch('success', 'Settings saved.'); $this->application->refresh(); + + // If port_exposes changed, reset default labels if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) { $this->resetDefaultLabels(false); } + if ($this->is_preserve_repository_enabled !== $this->application->settings->is_preserve_repository_enabled) { + if ($this->application->settings->is_preserve_repository_enabled === false) { + $this->application->fileStorages->each(function ($storage) { + $storage->is_based_on_git = $this->application->settings->is_preserve_repository_enabled; + $storage->save(); + }); + } + } } public function loadComposeFile($isInit = false) @@ -184,46 +195,24 @@ class General extends Component if ($isInit && $this->application->docker_compose_raw) { return; } - ['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation, 'initialDockerComposePrLocation' => $this->initialDockerComposePrLocation] = $this->application->loadComposeFile($isInit); + + // Must reload the application to get the latest database changes + // Why? Not sure, but it works. + // $this->application->refresh(); + + ['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation] = $this->application->loadComposeFile($isInit); if (is_null($this->parsedServices)) { $this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.'); return; } - $compose = $this->application->parseCompose(); - $services = data_get($compose, 'services'); - if ($services) { - $volumes = collect($services)->map(function ($service) { - return data_get($service, 'volumes'); - })->flatten()->filter(function ($volume) { - return str($volume)->startsWith('/data/coolify'); - })->unique()->values(); - foreach ($volumes as $volume) { - $source = Str::of($volume)->before(':'); - $target = Str::of($volume)->after(':')->beforeLast(':'); - - LocalFileVolume::updateOrCreate( - [ - 'mount_path' => $target, - 'resource_id' => $this->application->id, - 'resource_type' => get_class($this->application), - ], - [ - 'fs_path' => $source, - 'mount_path' => $target, - 'resource_id' => $this->application->id, - 'resource_type' => get_class($this->application), - ] - ); - } - } + $this->application->parse(); $this->dispatch('success', 'Docker compose file loaded.'); $this->dispatch('compose_loaded'); - $this->dispatch('refresh_storages'); + $this->dispatch('refreshStorages'); $this->dispatch('refreshEnvs'); } catch (\Throwable $e) { $this->application->docker_compose_location = $this->initialDockerComposeLocation; - $this->application->docker_compose_pr_location = $this->initialDockerComposePrLocation; $this->application->save(); return handleError($e, $this); @@ -234,7 +223,7 @@ class General extends Component public function generateDomain(string $serviceName) { - $uuid = new Cuid2(7); + $uuid = new Cuid2; $domain = generateFqdn($this->application->destination->server, $uuid); $this->parsedServiceDomains[$serviceName]['domain'] = $domain; $this->application->docker_compose_domains = json_encode($this->parsedServiceDomains); @@ -254,15 +243,11 @@ class General extends Component } } - public function updatedApplicationFqdn() + public function updatedApplicationSettingsIsStatic($value) { - $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { - return str($domain)->trim()->lower(); - }); - $this->application->fqdn = $this->application->fqdn->unique()->implode(','); - $this->resetDefaultLabels(); + if ($value) { + $this->generateNginxConfiguration(); + } } public function updatedApplicationBuildPack() @@ -281,6 +266,7 @@ class General extends Component if ($this->application->build_pack === 'static') { $this->application->ports_exposes = $this->ports_exposes = 80; $this->resetDefaultLabels(false); + $this->generateNginxConfiguration(); } $this->submit(); $this->dispatch('buildPackUpdated'); @@ -298,17 +284,31 @@ class General extends Component } } - public function resetDefaultLabels() + public function generateNginxConfiguration() { - $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); - $this->ports_exposes = $this->application->ports_exposes; - $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled; - $this->application->custom_labels = base64_encode($this->customLabels); + $this->application->custom_nginx_configuration = defaultNginxConfiguration(); $this->application->save(); - if ($this->application->build_pack === 'dockercompose') { - $this->loadComposeFile(); + $this->dispatch('success', 'Nginx configuration generated.'); + } + + public function resetDefaultLabels($manualReset = false) + { + try { + if ($this->application->settings->is_container_label_readonly_enabled && ! $manualReset) { + return; + } + $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); + $this->ports_exposes = $this->application->ports_exposes; + $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled; + $this->application->custom_labels = base64_encode($this->customLabels); + $this->application->save(); + if ($this->application->build_pack === 'dockercompose') { + $this->loadComposeFile(); + } + $this->dispatch('configurationChanged'); + } catch (\Throwable $e) { + return handleError($e, $this); } - $this->dispatch('configurationChanged'); } public function checkFqdns($showToaster = true) @@ -347,19 +347,29 @@ class General extends Component public function submit($showToaster = true) { try { - $this->set_redirect(); $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { + Url::fromString($domain, ['http', 'https']); + return str($domain)->trim()->lower(); }); + $this->application->fqdn = $this->application->fqdn->unique()->implode(','); + $warning = sslipDomainWarning($this->application->fqdn); + if ($warning) { + $this->dispatch('warning', __('warning.sslipdomain')); + } + $this->resetDefaultLabels(); + + if ($this->application->isDirty('redirect')) { + $this->set_redirect(); + } $this->checkFqdns(); $this->application->save(); - - if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') { + if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) { $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); @@ -372,6 +382,7 @@ class General extends Component } } $this->validate(); + if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) { $this->resetDefaultLabels(); } @@ -398,6 +409,7 @@ class General extends Component } if ($this->application->build_pack === 'dockercompose') { $this->application->docker_compose_domains = json_encode($this->parsedServiceDomains); + foreach ($this->parsedServiceDomains as $serviceName => $service) { $domain = data_get($service, 'domain'); if ($domain) { @@ -407,14 +419,35 @@ class General extends Component check_domain_usage(resource: $this->application); } } + if ($this->application->isDirty('docker_compose_domains')) { + $this->resetDefaultLabels(); + } } $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); - $showToaster && $this->dispatch('success', 'Application settings updated!'); + $showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!'); } catch (\Throwable $e) { + $originalFqdn = $this->application->getOriginal('fqdn'); + if ($originalFqdn !== $this->application->fqdn) { + $this->application->fqdn = $originalFqdn; + } + return handleError($e, $this); } finally { $this->dispatch('configurationChanged'); } } + + public function downloadConfig() + { + $config = GenerateConfig::run($this->application, true); + $fileName = str($this->application->name)->slug()->append('_config.json'); + + return response()->streamDownload(function () use ($config) { + echo $config; + }, $fileName, [ + 'Content-Type' => 'application/json', + 'Content-Disposition' => 'attachment; filename='.$fileName, + ]); + } } diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php index d224f4a9d..1082b48cd 100644 --- a/app/Livewire/Project/Application/Heading.php +++ b/app/Livewire/Project/Application/Heading.php @@ -5,8 +5,6 @@ namespace App\Livewire\Project\Application; use App\Actions\Application\StopApplication; use App\Actions\Docker\GetContainersStatus; use App\Events\ApplicationStatusChanged; -use App\Jobs\ContainerStatusJob; -use App\Jobs\ServerStatusJob; use App\Models\Application; use Livewire\Component; use Visus\Cuid2\Cuid2; @@ -23,6 +21,8 @@ class Heading extends Component protected string $deploymentUuid; + public bool $docker_cleanup = true; + public function getListeners() { $teamId = auth()->user()->currentTeam()->id; @@ -45,12 +45,8 @@ class Heading extends Component public function check_status($showNotification = false) { if ($this->application->destination->server->isFunctional()) { - GetContainersStatus::dispatch($this->application->destination->server); - // dispatch(new ContainerStatusJob($this->application->destination->server)); - } else { - dispatch(new ServerStatusJob($this->application->destination->server)); + GetContainersStatus::dispatch($this->application->destination->server)->onQueue('high'); } - if ($showNotification) { $this->dispatch('success', 'Success', 'Application status updated.'); } @@ -102,13 +98,13 @@ class Heading extends Component protected function setDeploymentUuid() { - $this->deploymentUuid = new Cuid2(7); + $this->deploymentUuid = new Cuid2; $this->parameters['deployment_uuid'] = $this->deploymentUuid; } public function stop() { - StopApplication::run($this->application); + StopApplication::run($this->application, false, $this->docker_cleanup); $this->application->status = 'exited'; $this->application->save(); if ($this->application->additional_servers->count() > 0) { @@ -141,4 +137,13 @@ class Heading extends Component 'environment_name' => $this->parameters['environment_name'], ]); } + + public function render() + { + return view('livewire.project.application.heading', [ + 'checkboxes' => [ + ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')], + ], + ]); + } } diff --git a/app/Livewire/Project/Application/Preview/Form.php b/app/Livewire/Project/Application/Preview/Form.php index cf5ab9c82..edcab44c8 100644 --- a/app/Livewire/Project/Application/Preview/Form.php +++ b/app/Livewire/Project/Application/Preview/Form.php @@ -3,7 +3,7 @@ namespace App\Livewire\Project\Application\Preview; use App\Models\Application; -use Illuminate\Support\Str; +use Livewire\Attributes\Validate; use Livewire\Component; use Spatie\Url\Url; @@ -11,45 +11,53 @@ class Form extends Component { public Application $application; - public string $preview_url_template; - - protected $rules = [ - 'application.preview_url_template' => 'required', - ]; - - protected $validationAttributes = [ - 'application.preview_url_template' => 'preview url template', - ]; - - public function resetToDefault() - { - $this->application->preview_url_template = '{{pr_id}}.{{domain}}'; - $this->preview_url_template = $this->application->preview_url_template; - $this->application->save(); - $this->generate_real_url(); - } - - public function generate_real_url() - { - if (data_get($this->application, 'fqdn')) { - $firstFqdn = Str::of($this->application->fqdn)->before(','); - $url = Url::fromString($firstFqdn); - $host = $url->getHost(); - $this->preview_url_template = Str::of($this->application->preview_url_template)->replace('{{domain}}', $host); - } - } + #[Validate('required')] + public string $previewUrlTemplate; public function mount() { - $this->generate_real_url(); + try { + $this->previewUrlTemplate = $this->application->preview_url_template; + $this->generateRealUrl(); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function submit() { - $this->validate(); - $this->application->preview_url_template = str_replace(' ', '', $this->application->preview_url_template); - $this->application->save(); - $this->dispatch('success', 'Preview url template updated.'); - $this->generate_real_url(); + try { + $this->resetErrorBag(); + $this->validate(); + $this->application->preview_url_template = str_replace(' ', '', $this->previewUrlTemplate); + $this->application->save(); + $this->dispatch('success', 'Preview url template updated.'); + $this->generateRealUrl(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function resetToDefault() + { + try { + $this->application->preview_url_template = '{{pr_id}}.{{domain}}'; + $this->previewUrlTemplate = $this->application->preview_url_template; + $this->application->save(); + $this->generateRealUrl(); + $this->dispatch('success', 'Preview url template updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function generateRealUrl() + { + if (data_get($this->application, 'fqdn')) { + $firstFqdn = str($this->application->fqdn)->before(','); + $url = Url::fromString($firstFqdn); + $host = $url->getHost(); + $this->previewUrlTemplate = str($this->application->preview_url_template)->replace('{{domain}}', $host); + } } } diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index ca911339e..d42bf03d7 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -5,7 +5,10 @@ namespace App\Livewire\Project\Application; use App\Actions\Docker\GetContainersStatus; use App\Models\Application; use App\Models\ApplicationPreview; +use Carbon\Carbon; +use Illuminate\Process\InvokedProcess; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Process; use Livewire\Component; use Spatie\Url\Url; use Visus\Cuid2\Cuid2; @@ -79,13 +82,20 @@ class Previews extends Component return; } - $fqdn = generateFqdn($this->application->destination->server, $this->application->uuid); + if ($this->application->build_pack === 'dockercompose') { + $preview->generate_preview_fqdn_compose(); + $this->application->refresh(); + $this->dispatch('success', 'Domain generated.'); + return; + } + + $fqdn = generateFqdn($this->application->destination->server, $this->application->uuid); $url = Url::fromString($fqdn); $template = $this->application->preview_url_template; $host = $url->getHost(); $schema = $url->getScheme(); - $random = new Cuid2(7); + $random = new Cuid2; $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $preview->pull_request_id, $preview_fqdn); @@ -131,6 +141,12 @@ class Previews extends Component } } + public function add_and_deploy(int $pull_request_id, ?string $pull_request_html_url = null) + { + $this->add($pull_request_id, $pull_request_html_url); + $this->deploy($pull_request_id, $pull_request_html_url); + } + public function deploy(int $pull_request_id, ?string $pull_request_html_url = null) { try { @@ -164,24 +180,27 @@ class Previews extends Component protected function setDeploymentUuid() { - $this->deployment_uuid = new Cuid2(7); + $this->deployment_uuid = new Cuid2; $this->parameters['deployment_uuid'] = $this->deployment_uuid; } public function stop(int $pull_request_id) { try { + $server = $this->application->destination->server; + $timeout = 300; + if ($this->application->destination->server->isSwarm()) { - instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server); + instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server); } else { - $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id); - foreach ($containers as $container) { - $name = str_replace('/', '', $container['Names']); - instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false); - } + $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray(); + $this->stopContainers($containers, $server, $timeout); } - GetContainersStatus::dispatchSync($this->application->destination->server); - $this->dispatch('reloadWindow'); + + GetContainersStatus::run($server); + $this->application->refresh(); + $this->dispatch('containerStatusUpdated'); + $this->dispatch('success', 'Preview Deployment stopped.'); } catch (\Throwable $e) { return handleError($e, $this); } @@ -190,16 +209,21 @@ class Previews extends Component public function delete(int $pull_request_id) { try { + $server = $this->application->destination->server; + $timeout = 300; + if ($this->application->destination->server->isSwarm()) { - instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server); + instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server); } else { - $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id); - foreach ($containers as $container) { - $name = str_replace('/', '', $container['Names']); - instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false); - } + $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray(); + $this->stopContainers($containers, $server, $timeout); } - ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete(); + + ApplicationPreview::where('application_id', $this->application->id) + ->where('pull_request_id', $pull_request_id) + ->first() + ->delete(); + $this->application->refresh(); $this->dispatch('update_links'); $this->dispatch('success', 'Preview deleted.'); @@ -207,4 +231,49 @@ class Previews extends Component return handleError($e, $this); } } + + private function stopContainers(array $containers, $server, int $timeout) + { + $processes = []; + foreach ($containers as $container) { + $containerName = str_replace('/', '', $container['Names']); + $processes[$containerName] = $this->stopContainer($containerName, $timeout); + } + + $startTime = Carbon::now()->getTimestamp(); + while (count($processes) > 0) { + $finishedProcesses = array_filter($processes, function ($process) { + return ! $process->running(); + }); + foreach (array_keys($finishedProcesses) as $containerName) { + unset($processes[$containerName]); + $this->removeContainer($containerName, $server); + } + + if (Carbon::now()->getTimestamp() - $startTime >= $timeout) { + $this->forceStopRemainingContainers(array_keys($processes), $server); + break; + } + + usleep(100000); + } + } + + private function stopContainer(string $containerName, int $timeout): InvokedProcess + { + return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); + } + + private function removeContainer(string $containerName, $server) + { + instant_remote_process(["docker rm -f $containerName"], $server, throwError: false); + } + + private function forceStopRemainingContainers(array $containerNames, $server) + { + foreach ($containerNames as $containerName) { + instant_remote_process(["docker kill $containerName"], $server, throwError: false); + $this->removeContainer($containerName, $server); + } + } } diff --git a/app/Livewire/Project/Application/PreviewsCompose.php b/app/Livewire/Project/Application/PreviewsCompose.php index bf4478e53..b3e838bb3 100644 --- a/app/Livewire/Project/Application/PreviewsCompose.php +++ b/app/Livewire/Project/Application/PreviewsCompose.php @@ -44,7 +44,7 @@ class PreviewsCompose extends Component $template = $this->preview->application->preview_url_template; $host = $url->getHost(); $schema = $url->getScheme(); - $random = new Cuid2(7); + $random = new Cuid2; $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn); diff --git a/app/Livewire/Project/Application/Rollback.php b/app/Livewire/Project/Application/Rollback.php index 41fe598b1..1e58a1458 100644 --- a/app/Livewire/Project/Application/Rollback.php +++ b/app/Livewire/Project/Application/Rollback.php @@ -3,7 +3,6 @@ namespace App\Livewire\Project\Application; use App\Models\Application; -use Illuminate\Support\Str; use Livewire\Component; use Visus\Cuid2\Cuid2; @@ -24,7 +23,7 @@ class Rollback extends Component public function rollbackImage($commit) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $this->application, @@ -50,16 +49,16 @@ class Rollback extends Component $output = instant_remote_process([ "docker inspect --format='{{.Config.Image}}' {$this->application->uuid}", ], $this->application->destination->server, throwError: false); - $current_tag = Str::of($output)->trim()->explode(':'); + $current_tag = str($output)->trim()->explode(':'); $this->current = data_get($current_tag, 1); $output = instant_remote_process([ "docker images --format '{{.Repository}}#{{.Tag}}#{{.CreatedAt}}'", ], $this->application->destination->server); - $this->images = Str::of($output)->trim()->explode("\n")->filter(function ($item) use ($image) { - return Str::of($item)->contains($image); + $this->images = str($output)->trim()->explode("\n")->filter(function ($item) use ($image) { + return str($item)->contains($image); })->map(function ($item) { - $item = Str::of($item)->explode('#'); + $item = str($item)->explode('#'); if ($item[1] === $this->current) { // $is_current = true; } diff --git a/app/Livewire/Project/Application/Source.php b/app/Livewire/Project/Application/Source.php index 426626e55..ade297d50 100644 --- a/app/Livewire/Project/Application/Source.php +++ b/app/Livewire/Project/Application/Source.php @@ -4,55 +4,92 @@ namespace App\Livewire\Project\Application; use App\Models\Application; use App\Models\PrivateKey; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class Source extends Component { - public $applicationId; - public Application $application; - public $private_keys; + #[Locked] + public $privateKeys; - protected $rules = [ - 'application.git_repository' => 'required', - 'application.git_branch' => 'required', - 'application.git_commit_sha' => 'nullable', - ]; + #[Validate(['nullable', 'string'])] + public ?string $privateKeyName = null; - protected $validationAttributes = [ - 'application.git_repository' => 'repository', - 'application.git_branch' => 'branch', - 'application.git_commit_sha' => 'commit sha', - ]; + #[Validate(['nullable', 'integer'])] + public ?int $privateKeyId = null; + + #[Validate(['required', 'string'])] + public string $gitRepository; + + #[Validate(['required', 'string'])] + public string $gitBranch; + + #[Validate(['nullable', 'string'])] + public ?string $gitCommitSha = null; public function mount() { - $this->get_private_keys(); + try { + $this->syncData(); + $this->getPrivateKeys(); + } catch (\Throwable $e) { + handleError($e, $this); + } } - private function get_private_keys() + public function syncData(bool $toModel = false) { - $this->private_keys = PrivateKey::whereTeamId(currentTeam()->id)->get()->reject(function ($key) { - return $key->id == $this->application->private_key_id; + if ($toModel) { + $this->validate(); + $this->application->update([ + 'git_repository' => $this->gitRepository, + 'git_branch' => $this->gitBranch, + 'git_commit_sha' => $this->gitCommitSha, + 'private_key_id' => $this->privateKeyId, + ]); + } else { + $this->gitRepository = $this->application->git_repository; + $this->gitBranch = $this->application->git_branch; + $this->gitCommitSha = $this->application->git_commit_sha; + $this->privateKeyId = $this->application->private_key_id; + $this->privateKeyName = data_get($this->application, 'private_key.name'); + } + } + + private function getPrivateKeys() + { + $this->privateKeys = PrivateKey::whereTeamId(currentTeam()->id)->get()->reject(function ($key) { + return $key->id == $this->privateKeyId; }); } - public function setPrivateKey(int $private_key_id) + public function setPrivateKey(int $privateKeyId) { - $this->application->private_key_id = $private_key_id; - $this->application->save(); - $this->application->refresh(); - $this->get_private_keys(); + try { + $this->privateKeyId = $privateKeyId; + $this->syncData(true); + $this->getPrivateKeys(); + $this->application->refresh(); + $this->privateKeyName = $this->application->private_key->name; + $this->dispatch('success', 'Private key updated!'); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function submit() { - $this->validate(); - if (! $this->application->git_commit_sha) { - $this->application->git_commit_sha = 'HEAD'; + try { + if (str($this->gitCommitSha)->isEmpty()) { + $this->gitCommitSha = 'HEAD'; + } + $this->syncData(true); + $this->dispatch('success', 'Application source updated!'); + } catch (\Throwable $e) { + return handleError($e, $this); } - $this->application->save(); - $this->dispatch('success', 'Application source updated!'); } } diff --git a/app/Livewire/Project/Application/Swarm.php b/app/Livewire/Project/Application/Swarm.php index 0151b5222..197dc41ed 100644 --- a/app/Livewire/Project/Application/Swarm.php +++ b/app/Livewire/Project/Application/Swarm.php @@ -3,32 +3,55 @@ namespace App\Livewire\Project\Application; use App\Models\Application; +use Livewire\Attributes\Validate; use Livewire\Component; class Swarm extends Component { public Application $application; - public string $swarm_placement_constraints = ''; + #[Validate('required')] + public int $swarmReplicas; - protected $rules = [ - 'application.swarm_replicas' => 'required', - 'application.swarm_placement_constraints' => 'nullable', - 'application.settings.is_swarm_only_worker_nodes' => 'required', - ]; + #[Validate(['nullable'])] + public ?string $swarmPlacementConstraints = null; + + #[Validate('required')] + public bool $isSwarmOnlyWorkerNodes; public function mount() { - if ($this->application->swarm_placement_constraints) { - $this->swarm_placement_constraints = base64_decode($this->application->swarm_placement_constraints); + try { + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->application->swarm_replicas = $this->swarmReplicas; + $this->application->swarm_placement_constraints = $this->swarmPlacementConstraints ? base64_encode($this->swarmPlacementConstraints) : null; + $this->application->settings->is_swarm_only_worker_nodes = $this->isSwarmOnlyWorkerNodes; + $this->application->save(); + $this->application->settings->save(); + } else { + $this->swarmReplicas = $this->application->swarm_replicas; + if ($this->application->swarm_placement_constraints) { + $this->swarmPlacementConstraints = base64_decode($this->application->swarm_placement_constraints); + } else { + $this->swarmPlacementConstraints = null; + } + $this->isSwarmOnlyWorkerNodes = $this->application->settings->is_swarm_only_worker_nodes; } } public function instantSave() { try { - $this->validate(); - $this->application->settings->save(); + $this->syncData(true); $this->dispatch('success', 'Swarm settings updated.'); } catch (\Throwable $e) { return handleError($e, $this); @@ -38,14 +61,7 @@ class Swarm extends Component public function submit() { try { - $this->validate(); - if ($this->swarm_placement_constraints) { - $this->application->swarm_placement_constraints = base64_encode($this->swarm_placement_constraints); - } else { - $this->application->swarm_placement_constraints = null; - } - $this->application->save(); - + $this->syncData(true); $this->dispatch('success', 'Swarm settings updated.'); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php index 5373f1b3f..4d2bc6589 100644 --- a/app/Livewire/Project/CloneMe.php +++ b/app/Livewire/Project/CloneMe.php @@ -47,7 +47,7 @@ class CloneMe extends Component $this->environment = $this->project->environments->where('name', $this->environment_name)->first(); $this->project_id = $this->project->id; $this->servers = currentTeam()->servers; - $this->newName = str($this->project->name.'-clone-'.(string) new Cuid2(7))->slug(); + $this->newName = str($this->project->name.'-clone-'.(string) new Cuid2)->slug(); } public function render() @@ -106,7 +106,7 @@ class CloneMe extends Component $databases = $this->environment->databases(); $services = $this->environment->services; foreach ($applications as $application) { - $uuid = (string) new Cuid2(7); + $uuid = (string) new Cuid2; $newApplication = $application->replicate()->fill([ 'uuid' => $uuid, 'fqdn' => generateFqdn($this->server, $uuid), @@ -133,7 +133,7 @@ class CloneMe extends Component } } foreach ($databases as $database) { - $uuid = (string) new Cuid2(7); + $uuid = (string) new Cuid2; $newDatabase = $database->replicate()->fill([ 'uuid' => $uuid, 'status' => 'exited', @@ -161,7 +161,7 @@ class CloneMe extends Component } } foreach ($services as $service) { - $uuid = (string) new Cuid2(7); + $uuid = (string) new Cuid2; $newService = $service->replicate()->fill([ 'uuid' => $uuid, 'environment_id' => $environment->id, diff --git a/app/Livewire/Project/Database/Backup/Index.php b/app/Livewire/Project/Database/Backup/Index.php index d9a4b623d..9ff2f48d5 100644 --- a/app/Livewire/Project/Database/Backup/Index.php +++ b/app/Livewire/Project/Database/Backup/Index.php @@ -24,10 +24,10 @@ class Index extends Component } // No backups if ( - $database->getMorphClass() === 'App\Models\StandaloneRedis' || - $database->getMorphClass() === 'App\Models\StandaloneKeydb' || - $database->getMorphClass() === 'App\Models\StandaloneDragonfly' || - $database->getMorphClass() === 'App\Models\StandaloneClickhouse' + $database->getMorphClass() === \App\Models\StandaloneRedis::class || + $database->getMorphClass() === \App\Models\StandaloneKeydb::class || + $database->getMorphClass() === \App\Models\StandaloneDragonfly::class || + $database->getMorphClass() === \App\Models\StandaloneClickhouse::class ) { return redirect()->route('project.database.configuration', [ 'project_uuid' => $project->uuid, diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index 59f2f9a39..b3a54f0ab 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -2,55 +2,113 @@ namespace App\Livewire\Project\Database; +use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; +use Exception; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; use Spatie\Url\Url; class BackupEdit extends Component { - public ?ScheduledDatabaseBackup $backup; + public ScheduledDatabaseBackup $backup; + #[Locked] public $s3s; + #[Locked] + public $parameters; + + #[Validate(['required', 'boolean'])] + public bool $delete_associated_backups_locally = false; + + #[Validate(['required', 'boolean'])] + public bool $delete_associated_backups_s3 = false; + + #[Validate(['required', 'boolean'])] + public bool $delete_associated_backups_sftp = false; + + #[Validate(['nullable', 'string'])] public ?string $status = null; - public array $parameters; + #[Validate(['required', 'boolean'])] + public bool $backupEnabled = false; - protected $rules = [ - 'backup.enabled' => 'required|boolean', - 'backup.frequency' => 'required|string', - 'backup.number_of_backups_locally' => 'required|integer|min:1', - 'backup.save_s3' => 'required|boolean', - 'backup.s3_storage_id' => 'nullable|integer', - 'backup.databases_to_backup' => 'nullable', - ]; + #[Validate(['required', 'string'])] + public string $frequency = ''; - protected $validationAttributes = [ - 'backup.enabled' => 'Enabled', - 'backup.frequency' => 'Frequency', - 'backup.number_of_backups_locally' => 'Number of Backups Locally', - 'backup.save_s3' => 'Save to S3', - 'backup.s3_storage_id' => 'S3 Storage', - 'backup.databases_to_backup' => 'Databases to Backup', - ]; + #[Validate(['required', 'integer', 'min:1'])] + public int $numberOfBackupsLocally = 1; - protected $messages = [ - 'backup.s3_storage_id' => 'Select a S3 Storage', - ]; + #[Validate(['required', 'boolean'])] + public bool $saveS3 = false; + + #[Validate(['nullable', 'integer'])] + public ?int $s3StorageId = 1; + + #[Validate(['nullable', 'string'])] + public ?string $databasesToBackup = null; + + #[Validate(['required', 'boolean'])] + public bool $dumpAll = false; public function mount() { - $this->parameters = get_route_parameters(); - if (is_null(data_get($this->backup, 's3_storage_id'))) { - data_set($this->backup, 's3_storage_id', 'default'); + try { + $this->parameters = get_route_parameters(); + $this->syncData(); + } catch (Exception $e) { + return handleError($e, $this); } } - public function delete() + public function syncData(bool $toModel = false) { + if ($toModel) { + $this->customValidate(); + $this->backup->enabled = $this->backupEnabled; + $this->backup->frequency = $this->frequency; + $this->backup->number_of_backups_locally = $this->numberOfBackupsLocally; + $this->backup->save_s3 = $this->saveS3; + $this->backup->s3_storage_id = $this->s3StorageId; + $this->backup->databases_to_backup = $this->databasesToBackup; + $this->backup->dump_all = $this->dumpAll; + $this->backup->save(); + } else { + $this->backupEnabled = $this->backup->enabled; + $this->frequency = $this->backup->frequency; + $this->numberOfBackupsLocally = $this->backup->number_of_backups_locally; + $this->saveS3 = $this->backup->save_s3; + $this->s3StorageId = $this->backup->s3_storage_id; + $this->databasesToBackup = $this->backup->databases_to_backup; + $this->dumpAll = $this->backup->dump_all; + } + } + + public function delete($password) + { + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); + + return; + } + } + try { + if ($this->delete_associated_backups_locally) { + $this->deleteAssociatedBackupsLocally(); + } + if ($this->delete_associated_backups_s3) { + $this->deleteAssociatedBackupsS3(); + } + $this->backup->delete(); - if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') { + + if ($this->backup->database->getMorphClass() === \App\Models\ServiceDatabase::class) { $previousUrl = url()->previous(); $url = Url::fromString($previousUrl); $url = $url->withoutQueryParameter('selectedBackupId'); @@ -69,16 +127,14 @@ class BackupEdit extends Component public function instantSave() { try { - $this->custom_validate(); - $this->backup->save(); - $this->backup->refresh(); + $this->syncData(true); $this->dispatch('success', 'Backup updated successfully.'); } catch (\Throwable $e) { $this->dispatch('error', $e->getMessage()); } } - private function custom_validate() + private function customValidate() { if (! is_numeric($this->backup->s3_storage_id)) { $this->backup->s3_storage_id = null; @@ -93,15 +149,72 @@ class BackupEdit extends Component public function submit() { try { - $this->custom_validate(); - if ($this->backup->databases_to_backup == '' || $this->backup->databases_to_backup === null) { - $this->backup->databases_to_backup = null; - } - $this->backup->save(); - $this->backup->refresh(); - $this->dispatch('success', 'Backup updated successfully'); + $this->syncData(true); + $this->dispatch('success', 'Backup updated successfully.'); } catch (\Throwable $e) { $this->dispatch('error', $e->getMessage()); } } + + private function deleteAssociatedBackupsLocally() + { + $executions = $this->backup->executions; + $backupFolder = null; + + foreach ($executions as $execution) { + if ($this->backup->database->getMorphClass() === \App\Models\ServiceDatabase::class) { + $server = $this->backup->database->service->destination->server; + } else { + $server = $this->backup->database->destination->server; + } + + if (! $backupFolder) { + $backupFolder = dirname($execution->filename); + } + + delete_backup_locally($execution->filename, $server); + $execution->delete(); + } + + if (str($backupFolder)->isNotEmpty()) { + $this->deleteEmptyBackupFolder($backupFolder, $server); + } + } + + private function deleteAssociatedBackupsS3() + { + //Add function to delete backups from S3 + } + + private function deleteAssociatedBackupsSftp() + { + //Add function to delete backups from SFTP + } + + private function deleteEmptyBackupFolder($folderPath, $server) + { + $checkEmpty = instant_remote_process(["[ -z \"$(ls -A '$folderPath')\" ] && echo 'empty' || echo 'not empty'"], $server); + + if (trim($checkEmpty) === 'empty') { + instant_remote_process(["rmdir '$folderPath'"], $server); + + $parentFolder = dirname($folderPath); + $checkParentEmpty = instant_remote_process(["[ -z \"$(ls -A '$parentFolder')\" ] && echo 'empty' || echo 'not empty'"], $server); + + if (trim($checkParentEmpty) === 'empty') { + instant_remote_process(["rmdir '$parentFolder'"], $server); + } + } + } + + public function render() + { + return view('livewire.project.database.backup-edit', [ + 'checkboxes' => [ + ['id' => 'delete_associated_backups_locally', 'label' => __('database.delete_backups_locally')], + // ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected S3 Storage.'] + // ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected SFTP Storage.'] + ], + ]); + } } diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php index de1bac36f..f91b8bfaf 100644 --- a/app/Livewire/Project/Database/BackupExecutions.php +++ b/app/Livewire/Project/Database/BackupExecutions.php @@ -2,24 +2,32 @@ namespace App\Livewire\Project\Database; +use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; use Livewire\Component; class BackupExecutions extends Component { public ?ScheduledDatabaseBackup $backup = null; + public $database; + public $executions = []; public $setDeletableBackup; + public $delete_backup_s3 = true; + + public $delete_backup_sftp = true; + public function getListeners() { - $userId = auth()->user()->id; + $userId = Auth::id(); return [ "echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions', - 'deleteBackup', ]; } @@ -32,19 +40,37 @@ class BackupExecutions extends Component } } - public function deleteBackup($exeuctionId) + public function deleteBackup($executionId, $password) { - $execution = $this->backup->executions()->where('id', $exeuctionId)->first(); + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); + + return; + } + } + + $execution = $this->backup->executions()->where('id', $executionId)->first(); if (is_null($execution)) { $this->dispatch('error', 'Backup execution not found.'); return; } - if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') { + + if ($execution->scheduledDatabaseBackup->database->getMorphClass() === \App\Models\ServiceDatabase::class) { delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server); } else { delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server); } + + if ($this->delete_backup_s3) { + // Add logic to delete from S3 + } + + if ($this->delete_backup_sftp) { + // Add logic to delete from SFTP + } + $execution->delete(); $this->dispatch('success', 'Backup deleted.'); $this->refreshBackupExecutions(); @@ -58,7 +84,65 @@ class BackupExecutions extends Component public function refreshBackupExecutions(): void { if ($this->backup) { - $this->executions = $this->backup->executions()->get()->sortBy('created_at'); + $this->executions = $this->backup->executions()->get(); } } + + public function mount(ScheduledDatabaseBackup $backup) + { + $this->backup = $backup; + $this->database = $backup->database; + $this->refreshBackupExecutions(); + } + + public function server() + { + if ($this->database) { + $server = null; + + if ($this->database instanceof \App\Models\ServiceDatabase) { + $server = $this->database->service->destination->server; + } elseif ($this->database->destination && $this->database->destination->server) { + $server = $this->database->destination->server; + } + if ($server) { + return $server; + } + } + + return null; + } + + public function getServerTimezone() + { + $server = $this->server(); + if (! $server) { + return 'UTC'; + } + + return $server->settings->server_timezone; + } + + public function formatDateInServerTimezone($date) + { + $serverTimezone = $this->getServerTimezone(); + $dateObj = new \DateTime($date); + try { + $dateObj->setTimezone(new \DateTimeZone($serverTimezone)); + } catch (\Exception) { + $dateObj->setTimezone(new \DateTimeZone('UTC')); + } + + return $dateObj->format('Y-m-d H:i:s T'); + } + + public function render() + { + return view('livewire.project.database.backup-executions', [ + 'checkboxes' => [ + ['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'], + ['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'], + ], + ]); + } } diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php index 875a36141..2d39c5151 100644 --- a/app/Livewire/Project/Database/Clickhouse/General.php +++ b/app/Livewire/Project/Database/Clickhouse/General.php @@ -7,6 +7,8 @@ use App\Actions\Database\StopDatabaseProxy; use App\Models\Server; use App\Models\StandaloneClickhouse; use Exception; +use Illuminate\Support\Facades\Auth; +use Livewire\Attributes\Validate; use Livewire\Component; class General extends Component @@ -15,54 +17,106 @@ class General extends Component public StandaloneClickhouse $database; - public ?string $db_url = null; + #[Validate(['required', 'string'])] + public string $name; - public ?string $db_url_public = null; + #[Validate(['nullable', 'string'])] + public ?string $description = null; - protected $listeners = ['refresh']; + #[Validate(['required', 'string'])] + public string $clickhouseAdminUser; - protected $rules = [ - 'database.name' => 'required', - 'database.description' => 'nullable', - 'database.clickhouse_admin_user' => 'required', - 'database.clickhouse_admin_password' => 'required', - 'database.image' => 'required', - 'database.ports_mappings' => 'nullable', - 'database.is_public' => 'nullable|boolean', - 'database.public_port' => 'nullable|integer', - 'database.is_log_drain_enabled' => 'nullable|boolean', - ]; + #[Validate(['required', 'string'])] + public string $clickhouseAdminPassword; - protected $validationAttributes = [ - 'database.name' => 'Name', - 'database.description' => 'Description', - 'database.clickhouse_admin_user' => 'Postgres User', - 'database.clickhouse_admin_password' => 'Postgres Password', - 'database.image' => 'Image', - 'database.ports_mappings' => 'Port Mapping', - 'database.is_public' => 'Is Public', - 'database.public_port' => 'Public Port', - ]; + #[Validate(['required', 'string'])] + public string $image; + + #[Validate(['nullable', 'string'])] + public ?string $portsMappings = null; + + #[Validate(['nullable', 'boolean'])] + public ?bool $isPublic = null; + + #[Validate(['nullable', 'integer'])] + public ?int $publicPort = null; + + #[Validate(['nullable', 'string'])] + public ?string $customDockerRunOptions = null; + + #[Validate(['nullable', 'string'])] + public ?string $dbUrl = null; + + #[Validate(['nullable', 'string'])] + public ?string $dbUrlPublic = null; + + #[Validate(['nullable', 'boolean'])] + public bool $isLogDrainEnabled = false; + + public function getListeners() + { + $teamId = Auth::user()->currentTeam()->id; + + return [ + "echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped', + ]; + } public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); + try { + $this->syncData(); + $this->server = data_get($this->database, 'destination.server'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->database->name = $this->name; + $this->database->description = $this->description; + $this->database->clickhouse_admin_user = $this->clickhouseAdminUser; + $this->database->clickhouse_admin_password = $this->clickhouseAdminPassword; + $this->database->image = $this->image; + $this->database->ports_mappings = $this->portsMappings; + $this->database->is_public = $this->isPublic; + $this->database->public_port = $this->publicPort; + $this->database->custom_docker_run_options = $this->customDockerRunOptions; + $this->database->is_log_drain_enabled = $this->isLogDrainEnabled; + $this->database->save(); + + $this->dbUrl = $this->database->internal_db_url; + $this->dbUrlPublic = $this->database->external_db_url; + } else { + $this->name = $this->database->name; + $this->description = $this->database->description; + $this->clickhouseAdminUser = $this->database->clickhouse_admin_user; + $this->clickhouseAdminPassword = $this->database->clickhouse_admin_password; + $this->image = $this->database->image; + $this->portsMappings = $this->database->ports_mappings; + $this->isPublic = $this->database->is_public; + $this->publicPort = $this->database->public_port; + $this->customDockerRunOptions = $this->database->custom_docker_run_options; + $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; + $this->dbUrl = $this->database->internal_db_url; + $this->dbUrlPublic = $this->database->external_db_url; } - $this->server = data_get($this->database, 'destination.server'); } public function instantSaveAdvanced() { try { if (! $this->server->isLogDrainEnabled()) { - $this->database->is_log_drain_enabled = false; + $this->isLogDrainEnabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); return; } - $this->database->save(); + $this->syncData(true); + $this->dispatch('success', 'Database updated.'); $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); } catch (Exception $e) { @@ -73,48 +127,47 @@ class General extends Component public function instantSave() { try { - if ($this->database->is_public && ! $this->database->public_port) { + if ($this->isPublic && ! $this->publicPort) { $this->dispatch('error', 'Public port is required.'); - $this->database->is_public = false; + $this->isPublic = false; return; } - if ($this->database->is_public) { + if ($this->isPublic) { if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); - $this->database->is_public = false; + $this->isPublic = false; return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } - $this->database->save(); + $this->dbUrlPublic = $this->database->external_db_url; + $this->syncData(true); } catch (\Throwable $e) { - $this->database->is_public = ! $this->database->is_public; + $this->isPublic = ! $this->isPublic; + $this->syncData(true); return handleError($e, $this); } } - public function refresh(): void + public function databaseProxyStopped() { - $this->database->refresh(); + $this->syncData(); } public function submit() { try { - if (str($this->database->public_port)->isEmpty()) { - $this->database->public_port = null; + if (str($this->publicPort)->isEmpty()) { + $this->publicPort = null; } - $this->validate(); - $this->database->save(); + $this->syncData(true); $this->dispatch('success', 'Database updated.'); } catch (Exception $e) { return handleError($e, $this); diff --git a/app/Livewire/Project/Database/CreateScheduledBackup.php b/app/Livewire/Project/Database/CreateScheduledBackup.php index 5ed74a6c3..0903efdfd 100644 --- a/app/Livewire/Project/Database/CreateScheduledBackup.php +++ b/app/Livewire/Project/Database/CreateScheduledBackup.php @@ -4,44 +4,45 @@ namespace App\Livewire\Project\Database; use App\Models\ScheduledDatabaseBackup; use Illuminate\Support\Collection; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class CreateScheduledBackup extends Component { - public $database; - + #[Validate(['required', 'string'])] public $frequency; + #[Validate(['required', 'boolean'])] + public bool $saveToS3 = false; + + #[Locked] + public $database; + public bool $enabled = true; - public bool $save_s3 = false; + #[Validate(['required', 'integer'])] + public int $s3StorageId; - public $s3_storage_id; - - public Collection $s3s; - - protected $rules = [ - 'frequency' => 'required|string', - 'save_s3' => 'required|boolean', - ]; - - protected $validationAttributes = [ - 'frequency' => 'Backup Frequency', - 'save_s3' => 'Save to S3', - ]; + public Collection $definedS3s; public function mount() { - $this->s3s = currentTeam()->s3s; - if ($this->s3s->count() > 0) { - $this->s3_storage_id = $this->s3s->first()->id; + try { + $this->definedS3s = currentTeam()->s3s; + if ($this->definedS3s->count() > 0) { + $this->s3StorageId = $this->definedS3s->first()->id; + } + } catch (\Throwable $e) { + return handleError($e, $this); } } - public function submit(): void + public function submit() { try { $this->validate(); + $isValid = validate_cron_expression($this->frequency); if (! $isValid) { $this->dispatch('error', 'Invalid Cron / Human expression.'); @@ -51,8 +52,8 @@ class CreateScheduledBackup extends Component $payload = [ 'enabled' => true, 'frequency' => $this->frequency, - 'save_s3' => $this->save_s3, - 's3_storage_id' => $this->s3_storage_id, + 'save_s3' => $this->saveToS3, + 's3_storage_id' => $this->s3StorageId, 'database_id' => $this->database->id, 'database_type' => $this->database->getMorphClass(), 'team_id' => currentTeam()->id, @@ -66,16 +67,16 @@ class CreateScheduledBackup extends Component } $databaseBackup = ScheduledDatabaseBackup::create($payload); - if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') { + if ($this->database->getMorphClass() === \App\Models\ServiceDatabase::class) { $this->dispatch('refreshScheduledBackups', $databaseBackup->id); } else { $this->dispatch('refreshScheduledBackups'); } } catch (\Throwable $e) { - handleError($e, $this); + return handleError($e, $this); } finally { $this->frequency = ''; - $this->save_s3 = true; + $this->saveToS3 = true; } } } diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php index d6c4eb2ce..ea6cd46b0 100644 --- a/app/Livewire/Project/Database/Dragonfly/General.php +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -7,60 +7,111 @@ use App\Actions\Database\StopDatabaseProxy; use App\Models\Server; use App\Models\StandaloneDragonfly; use Exception; +use Illuminate\Support\Facades\Auth; +use Livewire\Attributes\Validate; use Livewire\Component; class General extends Component { - protected $listeners = ['refresh']; - public Server $server; public StandaloneDragonfly $database; - public ?string $db_url = null; + #[Validate(['required', 'string'])] + public string $name; - public ?string $db_url_public = null; + #[Validate(['nullable', 'string'])] + public ?string $description = null; - protected $rules = [ - 'database.name' => 'required', - 'database.description' => 'nullable', - 'database.dragonfly_password' => 'required', - 'database.image' => 'required', - 'database.ports_mappings' => 'nullable', - 'database.is_public' => 'nullable|boolean', - 'database.public_port' => 'nullable|integer', - 'database.is_log_drain_enabled' => 'nullable|boolean', - ]; + #[Validate(['required', 'string'])] + public string $dragonflyPassword; - protected $validationAttributes = [ - 'database.name' => 'Name', - 'database.description' => 'Description', - 'database.dragonfly_password' => 'Redis Password', - 'database.image' => 'Image', - 'database.ports_mappings' => 'Port Mapping', - 'database.is_public' => 'Is Public', - 'database.public_port' => 'Public Port', - ]; + #[Validate(['required', 'string'])] + public string $image; + + #[Validate(['nullable', 'string'])] + public ?string $portsMappings = null; + + #[Validate(['nullable', 'boolean'])] + public ?bool $isPublic = null; + + #[Validate(['nullable', 'integer'])] + public ?int $publicPort = null; + + #[Validate(['nullable', 'string'])] + public ?string $customDockerRunOptions = null; + + #[Validate(['nullable', 'string'])] + public ?string $dbUrl = null; + + #[Validate(['nullable', 'string'])] + public ?string $dbUrlPublic = null; + + #[Validate(['nullable', 'boolean'])] + public bool $isLogDrainEnabled = false; + + public function getListeners() + { + $teamId = Auth::user()->currentTeam()->id; + + return [ + "echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped', + ]; + } public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); + try { + $this->syncData(); + $this->server = data_get($this->database, 'destination.server'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->database->name = $this->name; + $this->database->description = $this->description; + $this->database->dragonfly_password = $this->dragonflyPassword; + $this->database->image = $this->image; + $this->database->ports_mappings = $this->portsMappings; + $this->database->is_public = $this->isPublic; + $this->database->public_port = $this->publicPort; + $this->database->custom_docker_run_options = $this->customDockerRunOptions; + $this->database->is_log_drain_enabled = $this->isLogDrainEnabled; + $this->database->save(); + + $this->dbUrl = $this->database->internal_db_url; + $this->dbUrlPublic = $this->database->external_db_url; + } else { + $this->name = $this->database->name; + $this->description = $this->database->description; + $this->dragonflyPassword = $this->database->dragonfly_password; + $this->image = $this->database->image; + $this->portsMappings = $this->database->ports_mappings; + $this->isPublic = $this->database->is_public; + $this->publicPort = $this->database->public_port; + $this->customDockerRunOptions = $this->database->custom_docker_run_options; + $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; + $this->dbUrl = $this->database->internal_db_url; + $this->dbUrlPublic = $this->database->external_db_url; } - $this->server = data_get($this->database, 'destination.server'); } public function instantSaveAdvanced() { try { if (! $this->server->isLogDrainEnabled()) { - $this->database->is_log_drain_enabled = false; + $this->isLogDrainEnabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); return; } - $this->database->save(); + $this->syncData(true); + $this->dispatch('success', 'Database updated.'); $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); } catch (Exception $e) { @@ -68,11 +119,50 @@ class General extends Component } } + public function instantSave() + { + try { + if ($this->isPublic && ! $this->publicPort) { + $this->dispatch('error', 'Public port is required.'); + $this->isPublic = false; + + return; + } + if ($this->isPublic) { + if (! str($this->database->status)->startsWith('running')) { + $this->dispatch('error', 'Database must be started to be publicly accessible.'); + $this->isPublic = false; + + return; + } + StartDatabaseProxy::run($this->database); + $this->dispatch('success', 'Database is now publicly accessible.'); + } else { + StopDatabaseProxy::run($this->database); + $this->dispatch('success', 'Database is no longer publicly accessible.'); + } + $this->dbUrlPublic = $this->database->external_db_url; + $this->syncData(true); + } catch (\Throwable $e) { + $this->isPublic = ! $this->isPublic; + $this->syncData(true); + + return handleError($e, $this); + } + } + + public function databaseProxyStopped() + { + $this->syncData(); + } + public function submit() { try { - $this->validate(); - $this->database->save(); + if (str($this->publicPort)->isEmpty()) { + $this->publicPort = null; + } + $this->syncData(true); $this->dispatch('success', 'Database updated.'); } catch (Exception $e) { return handleError($e, $this); @@ -84,46 +174,4 @@ class General extends Component } } } - - public function instantSave() - { - try { - if ($this->database->is_public && ! $this->database->public_port) { - $this->dispatch('error', 'Public port is required.'); - $this->database->is_public = false; - - return; - } - if ($this->database->is_public) { - if (! str($this->database->status)->startsWith('running')) { - $this->dispatch('error', 'Database must be started to be publicly accessible.'); - $this->database->is_public = false; - - return; - } - StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); - $this->dispatch('success', 'Database is now publicly accessible.'); - } else { - StopDatabaseProxy::run($this->database); - $this->db_url_public = null; - $this->dispatch('success', 'Database is no longer publicly accessible.'); - } - $this->database->save(); - } catch (\Throwable $e) { - $this->database->is_public = ! $this->database->is_public; - - return handleError($e, $this); - } - } - - public function refresh(): void - { - $this->database->refresh(); - } - - public function render() - { - return view('livewire.project.database.dragonfly.general'); - } } diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php index 61dafa76f..fc0febd02 100644 --- a/app/Livewire/Project/Database/Heading.php +++ b/app/Livewire/Project/Database/Heading.php @@ -2,17 +2,11 @@ namespace App\Livewire\Project\Database; -use App\Actions\Database\StartClickhouse; -use App\Actions\Database\StartDragonfly; -use App\Actions\Database\StartKeydb; -use App\Actions\Database\StartMariadb; -use App\Actions\Database\StartMongodb; -use App\Actions\Database\StartMysql; -use App\Actions\Database\StartPostgresql; -use App\Actions\Database\StartRedis; +use App\Actions\Database\RestartDatabase; +use App\Actions\Database\StartDatabase; use App\Actions\Database\StopDatabase; use App\Actions\Docker\GetContainersStatus; -use App\Jobs\ContainerStatusJob; +use Illuminate\Support\Facades\Auth; use Livewire\Component; class Heading extends Component @@ -21,9 +15,11 @@ class Heading extends Component public array $parameters; + public $docker_cleanup = true; + public function getListeners() { - $userId = auth()->user()->id; + $userId = Auth::id(); return [ "echo-private:user.{$userId},DatabaseStatusChanged" => 'activityFinished', @@ -48,7 +44,6 @@ class Heading extends Component public function check_status($showNotification = false) { GetContainersStatus::run($this->database->destination->server); - // dispatch_sync(new ContainerStatusJob($this->database->destination->server)); $this->database->refresh(); if ($showNotification) { $this->dispatch('success', 'Database status updated.'); @@ -62,38 +57,30 @@ class Heading extends Component public function stop() { - StopDatabase::run($this->database); + StopDatabase::run($this->database, false, $this->docker_cleanup); $this->database->status = 'exited'; $this->database->save(); $this->check_status(); } + public function restart() + { + $activity = RestartDatabase::run($this->database); + $this->dispatch('activityMonitor', $activity->id); + } + public function start() { - if ($this->database->type() === 'standalone-postgresql') { - $activity = StartPostgresql::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } elseif ($this->database->type() === 'standalone-redis') { - $activity = StartRedis::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } elseif ($this->database->type() === 'standalone-mongodb') { - $activity = StartMongodb::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } elseif ($this->database->type() === 'standalone-mysql') { - $activity = StartMysql::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } elseif ($this->database->type() === 'standalone-mariadb') { - $activity = StartMariadb::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } elseif ($this->database->type() === 'standalone-keydb') { - $activity = StartKeydb::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } elseif ($this->database->type() === 'standalone-dragonfly') { - $activity = StartDragonfly::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } elseif ($this->database->type() === 'standalone-clickhouse') { - $activity = StartClickhouse::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } + $activity = StartDatabase::run($this->database); + $this->dispatch('activityMonitor', $activity->id); + } + + public function render() + { + return view('livewire.project.database.heading', [ + 'checkboxes' => [ + ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')], + ], + ]); } } diff --git a/app/Livewire/Project/Database/Import.php b/app/Livewire/Project/Database/Import.php index dfaa4461b..062f454b1 100644 --- a/app/Livewire/Project/Database/Import.php +++ b/app/Livewire/Project/Database/Import.php @@ -3,6 +3,7 @@ namespace App\Livewire\Project\Database; use App\Models\Server; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Storage; use Livewire\Component; @@ -46,7 +47,7 @@ class Import extends Component public function getListeners() { - $userId = auth()->user()->id; + $userId = Auth::id(); return [ "echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh', @@ -77,10 +78,10 @@ class Import extends Component } if ( - $this->resource->getMorphClass() == 'App\Models\StandaloneRedis' || - $this->resource->getMorphClass() == 'App\Models\StandaloneKeydb' || - $this->resource->getMorphClass() == 'App\Models\StandaloneDragonfly' || - $this->resource->getMorphClass() == 'App\Models\StandaloneClickhouse' + $this->resource->getMorphClass() === \App\Models\StandaloneRedis::class || + $this->resource->getMorphClass() === \App\Models\StandaloneKeydb::class || + $this->resource->getMorphClass() === \App\Models\StandaloneDragonfly::class || + $this->resource->getMorphClass() === \App\Models\StandaloneClickhouse::class ) { $this->unsupported = true; } @@ -88,8 +89,7 @@ class Import extends Component public function runImport() { - - if ($this->filename == '') { + if ($this->filename === '') { $this->dispatch('error', 'Please select a file to import.'); return; @@ -108,19 +108,19 @@ class Import extends Component $this->importCommands[] = "docker cp {$tmpPath} {$this->container}:{$tmpPath}"; switch ($this->resource->getMorphClass()) { - case 'App\Models\StandaloneMariadb': + case \App\Models\StandaloneMariadb::class: $this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mariadbRestoreCommand} < {$tmpPath}'"; $this->importCommands[] = "rm {$tmpPath}"; break; - case 'App\Models\StandaloneMysql': + case \App\Models\StandaloneMysql::class: $this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mysqlRestoreCommand} < {$tmpPath}'"; $this->importCommands[] = "rm {$tmpPath}"; break; - case 'App\Models\StandalonePostgresql': + case \App\Models\StandalonePostgresql::class: $this->importCommands[] = "docker exec {$this->container} sh -c '{$this->postgresqlRestoreCommand} {$tmpPath}'"; $this->importCommands[] = "rm {$tmpPath}"; break; - case 'App\Models\StandaloneMongodb': + case \App\Models\StandaloneMongodb::class: $this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mongodbRestoreCommand}{$tmpPath}'"; $this->importCommands[] = "rm {$tmpPath}"; break; diff --git a/app/Livewire/Project/Database/InitScript.php b/app/Livewire/Project/Database/InitScript.php index 336762981..e3baa1c8e 100644 --- a/app/Livewire/Project/Database/InitScript.php +++ b/app/Livewire/Project/Database/InitScript.php @@ -3,39 +3,39 @@ namespace App\Livewire\Project\Database; use Exception; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class InitScript extends Component { + #[Locked] public array $script; + #[Locked] public int $index; - public ?string $filename; + #[Validate(['nullable', 'string'])] + public ?string $filename = null; - public ?string $content; - - protected $rules = [ - 'filename' => 'required|string', - 'content' => 'required|string', - ]; - - protected $validationAttributes = [ - 'filename' => 'Filename', - 'content' => 'Content', - ]; + #[Validate(['nullable', 'string'])] + public ?string $content = null; public function mount() { - $this->index = data_get($this->script, 'index'); - $this->filename = data_get($this->script, 'filename'); - $this->content = data_get($this->script, 'content'); + try { + $this->index = data_get($this->script, 'index'); + $this->filename = data_get($this->script, 'filename'); + $this->content = data_get($this->script, 'content'); + } catch (Exception $e) { + return handleError($e, $this); + } } public function submit() { - $this->validate(); try { + $this->validate(); $this->script['index'] = $this->index; $this->script['content'] = $this->content; $this->script['filename'] = $this->filename; @@ -47,6 +47,10 @@ class InitScript extends Component public function delete() { - $this->dispatch('delete_init_script', $this->script); + try { + $this->dispatch('delete_init_script', $this->script); + } catch (Exception $e) { + return handleError($e, $this); + } } } diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php index 381711946..e768495eb 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -7,63 +7,116 @@ use App\Actions\Database\StopDatabaseProxy; use App\Models\Server; use App\Models\StandaloneKeydb; use Exception; +use Illuminate\Support\Facades\Auth; +use Livewire\Attributes\Validate; use Livewire\Component; class General extends Component { - protected $listeners = ['refresh']; - public Server $server; public StandaloneKeydb $database; - public ?string $db_url = null; + #[Validate(['required', 'string'])] + public string $name; - public ?string $db_url_public = null; + #[Validate(['nullable', 'string'])] + public ?string $description = null; - protected $rules = [ - 'database.name' => 'required', - 'database.description' => 'nullable', - 'database.keydb_conf' => 'nullable', - 'database.keydb_password' => 'required', - 'database.image' => 'required', - 'database.ports_mappings' => 'nullable', - 'database.is_public' => 'nullable|boolean', - 'database.public_port' => 'nullable|integer', - 'database.is_log_drain_enabled' => 'nullable|boolean', - ]; + #[Validate(['nullable', 'string'])] + public ?string $keydbConf = null; - protected $validationAttributes = [ - 'database.name' => 'Name', - 'database.description' => 'Description', - 'database.keydb_conf' => 'Redis Configuration', - 'database.keydb_password' => 'Redis Password', - 'database.image' => 'Image', - 'database.ports_mappings' => 'Port Mapping', - 'database.is_public' => 'Is Public', - 'database.public_port' => 'Public Port', - ]; + #[Validate(['required', 'string'])] + public string $keydbPassword; + + #[Validate(['required', 'string'])] + public string $image; + + #[Validate(['nullable', 'string'])] + public ?string $portsMappings = null; + + #[Validate(['nullable', 'boolean'])] + public ?bool $isPublic = null; + + #[Validate(['nullable', 'integer'])] + public ?int $publicPort = null; + + #[Validate(['nullable', 'string'])] + public ?string $customDockerRunOptions = null; + + #[Validate(['nullable', 'string'])] + public ?string $dbUrl = null; + + #[Validate(['nullable', 'string'])] + public ?string $dbUrlPublic = null; + + #[Validate(['nullable', 'boolean'])] + public bool $isLogDrainEnabled = false; + + public function getListeners() + { + $teamId = Auth::user()->currentTeam()->id; + + return [ + "echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped', + ]; + } public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); + try { + $this->syncData(); + $this->server = data_get($this->database, 'destination.server'); + } catch (\Throwable $e) { + return handleError($e, $this); } - $this->server = data_get($this->database, 'destination.server'); + } + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->database->name = $this->name; + $this->database->description = $this->description; + $this->database->keydb_conf = $this->keydbConf; + $this->database->keydb_password = $this->keydbPassword; + $this->database->image = $this->image; + $this->database->ports_mappings = $this->portsMappings; + $this->database->is_public = $this->isPublic; + $this->database->public_port = $this->publicPort; + $this->database->custom_docker_run_options = $this->customDockerRunOptions; + $this->database->is_log_drain_enabled = $this->isLogDrainEnabled; + $this->database->save(); + + $this->dbUrl = $this->database->internal_db_url; + $this->dbUrlPublic = $this->database->external_db_url; + } else { + $this->name = $this->database->name; + $this->description = $this->database->description; + $this->keydbConf = $this->database->keydb_conf; + $this->keydbPassword = $this->database->keydb_password; + $this->image = $this->database->image; + $this->portsMappings = $this->database->ports_mappings; + $this->isPublic = $this->database->is_public; + $this->publicPort = $this->database->public_port; + $this->customDockerRunOptions = $this->database->custom_docker_run_options; + $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; + $this->dbUrl = $this->database->internal_db_url; + $this->dbUrlPublic = $this->database->external_db_url; + } } public function instantSaveAdvanced() { try { if (! $this->server->isLogDrainEnabled()) { - $this->database->is_log_drain_enabled = false; + $this->isLogDrainEnabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); return; } - $this->database->save(); + $this->syncData(true); + $this->dispatch('success', 'Database updated.'); $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); } catch (Exception $e) { @@ -71,14 +124,50 @@ class General extends Component } } + public function instantSave() + { + try { + if ($this->isPublic && ! $this->publicPort) { + $this->dispatch('error', 'Public port is required.'); + $this->isPublic = false; + + return; + } + if ($this->isPublic) { + if (! str($this->database->status)->startsWith('running')) { + $this->dispatch('error', 'Database must be started to be publicly accessible.'); + $this->isPublic = false; + + return; + } + StartDatabaseProxy::run($this->database); + $this->dispatch('success', 'Database is now publicly accessible.'); + } else { + StopDatabaseProxy::run($this->database); + $this->dispatch('success', 'Database is no longer publicly accessible.'); + } + $this->dbUrlPublic = $this->database->external_db_url; + $this->syncData(true); + } catch (\Throwable $e) { + $this->isPublic = ! $this->isPublic; + $this->syncData(true); + + return handleError($e, $this); + } + } + + public function databaseProxyStopped() + { + $this->syncData(); + } + public function submit() { try { - $this->validate(); - if ($this->database->keydb_conf === '') { - $this->database->keydb_conf = null; + if (str($this->publicPort)->isEmpty()) { + $this->publicPort = null; } - $this->database->save(); + $this->syncData(true); $this->dispatch('success', 'Database updated.'); } catch (Exception $e) { return handleError($e, $this); @@ -90,46 +179,4 @@ class General extends Component } } } - - public function instantSave() - { - try { - if ($this->database->is_public && ! $this->database->public_port) { - $this->dispatch('error', 'Public port is required.'); - $this->database->is_public = false; - - return; - } - if ($this->database->is_public) { - if (! str($this->database->status)->startsWith('running')) { - $this->dispatch('error', 'Database must be started to be publicly accessible.'); - $this->database->is_public = false; - - return; - } - StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); - $this->dispatch('success', 'Database is now publicly accessible.'); - } else { - StopDatabaseProxy::run($this->database); - $this->db_url_public = null; - $this->dispatch('success', 'Database is no longer publicly accessible.'); - } - $this->database->save(); - } catch (\Throwable $e) { - $this->database->is_public = ! $this->database->is_public; - - return handleError($e, $this); - } - } - - public function refresh(): void - { - $this->database->refresh(); - } - - public function render() - { - return view('livewire.project.database.keydb.general'); - } } diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php index 8b4b35d11..c9d473223 100644 --- a/app/Livewire/Project/Database/Mariadb/General.php +++ b/app/Livewire/Project/Database/Mariadb/General.php @@ -34,6 +34,7 @@ class General extends Component 'database.is_public' => 'nullable|boolean', 'database.public_port' => 'nullable|integer', 'database.is_log_drain_enabled' => 'nullable|boolean', + 'database.custom_docker_run_options' => 'nullable', ]; protected $validationAttributes = [ @@ -48,16 +49,14 @@ class General extends Component 'database.ports_mappings' => 'Port Mapping', 'database.is_public' => 'Is Public', 'database.public_port' => 'Public Port', + 'database.custom_docker_run_options' => 'Custom Docker Options', ]; public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); - } + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); - } public function instantSaveAdvanced() @@ -114,13 +113,12 @@ class General extends Component return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } + $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { $this->database->is_public = ! $this->database->is_public; diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php index ee639ae41..e19895dae 100644 --- a/app/Livewire/Project/Database/Mongodb/General.php +++ b/app/Livewire/Project/Database/Mongodb/General.php @@ -33,6 +33,7 @@ class General extends Component 'database.is_public' => 'nullable|boolean', 'database.public_port' => 'nullable|integer', 'database.is_log_drain_enabled' => 'nullable|boolean', + 'database.custom_docker_run_options' => 'nullable', ]; protected $validationAttributes = [ @@ -46,16 +47,14 @@ class General extends Component 'database.ports_mappings' => 'Port Mapping', 'database.is_public' => 'Is Public', 'database.public_port' => 'Public Port', + 'database.custom_docker_run_options' => 'Custom Docker Run Options', ]; public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); - } + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); - } public function instantSaveAdvanced() @@ -115,13 +114,12 @@ class General extends Component return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } + $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { $this->database->is_public = ! $this->database->is_public; diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php index fc0767109..7d5270ddf 100644 --- a/app/Livewire/Project/Database/Mysql/General.php +++ b/app/Livewire/Project/Database/Mysql/General.php @@ -34,6 +34,7 @@ class General extends Component 'database.is_public' => 'nullable|boolean', 'database.public_port' => 'nullable|integer', 'database.is_log_drain_enabled' => 'nullable|boolean', + 'database.custom_docker_run_options' => 'nullable', ]; protected $validationAttributes = [ @@ -48,14 +49,13 @@ class General extends Component 'database.ports_mappings' => 'Port Mapping', 'database.is_public' => 'Is Public', 'database.public_port' => 'Public Port', + 'database.custom_docker_run_options' => 'Custom Docker Run Options', ]; public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); - } + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); } @@ -113,13 +113,12 @@ class General extends Component return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } + $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { $this->database->is_public = ! $this->database->is_public; diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php index 38cac2e5c..c12fa49f3 100644 --- a/app/Livewire/Project/Database/Postgresql/General.php +++ b/app/Livewire/Project/Database/Postgresql/General.php @@ -25,7 +25,14 @@ class General extends Component public ?string $db_url_public = null; - protected $listeners = ['refresh', 'save_init_script', 'delete_init_script']; + public function getListeners() + { + return [ + 'refresh', + 'save_init_script', + 'delete_init_script', + ]; + } protected $rules = [ 'database.name' => 'required', @@ -42,6 +49,7 @@ class General extends Component 'database.is_public' => 'nullable|boolean', 'database.public_port' => 'nullable|integer', 'database.is_log_drain_enabled' => 'nullable|boolean', + 'database.custom_docker_run_options' => 'nullable', ]; protected $validationAttributes = [ @@ -58,14 +66,13 @@ class General extends Component 'database.ports_mappings' => 'Port Mapping', 'database.is_public' => 'Is Public', 'database.public_port' => 'Public Port', + 'database.custom_docker_run_options' => 'Custom Docker Run Options', ]; public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); - } + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); } @@ -103,13 +110,12 @@ class General extends Component return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } + $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { $this->database->is_public = ! $this->database->is_public; diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index b5c1dd881..25a96b292 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -11,12 +11,21 @@ use Livewire\Component; class General extends Component { - protected $listeners = ['refresh']; + protected $listeners = [ + 'envsUpdated' => 'refresh', + 'refresh', + ]; public Server $server; public StandaloneRedis $database; + public string $redis_username; + + public string $redis_password; + + public string $redis_version; + public ?string $db_url = null; public ?string $db_url_public = null; @@ -25,33 +34,33 @@ class General extends Component 'database.name' => 'required', 'database.description' => 'nullable', 'database.redis_conf' => 'nullable', - 'database.redis_password' => 'required', 'database.image' => 'required', 'database.ports_mappings' => 'nullable', 'database.is_public' => 'nullable|boolean', 'database.public_port' => 'nullable|integer', 'database.is_log_drain_enabled' => 'nullable|boolean', + 'database.custom_docker_run_options' => 'nullable', + 'redis_username' => 'required', + 'redis_password' => 'required', ]; protected $validationAttributes = [ 'database.name' => 'Name', 'database.description' => 'Description', 'database.redis_conf' => 'Redis Configuration', - 'database.redis_password' => 'Redis Password', 'database.image' => 'Image', 'database.ports_mappings' => 'Port Mapping', 'database.is_public' => 'Is Public', 'database.public_port' => 'Public Port', + 'database.custom_docker_run_options' => 'Custom Docker Options', + 'redis_username' => 'Redis Username', + 'redis_password' => 'Redis Password', ]; public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); - } $this->server = data_get($this->database, 'destination.server'); - + $this->refreshView(); } public function instantSaveAdvanced() @@ -75,13 +84,24 @@ class General extends Component { try { $this->validate(); - if ($this->database->redis_conf === '') { - $this->database->redis_conf = null; + + if (version_compare($this->redis_version, '6.0', '>=')) { + $this->database->runtime_environment_variables()->updateOrCreate( + ['key' => 'REDIS_USERNAME'], + ['value' => $this->redis_username, 'standalone_redis_id' => $this->database->id] + ); } + $this->database->runtime_environment_variables()->updateOrCreate( + ['key' => 'REDIS_PASSWORD'], + ['value' => $this->redis_password, 'standalone_redis_id' => $this->database->id] + ); + $this->database->save(); $this->dispatch('success', 'Database updated.'); } catch (Exception $e) { return handleError($e, $this); + } finally { + $this->dispatch('refreshEnvs'); } } @@ -102,13 +122,12 @@ class General extends Component return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } + $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { $this->database->is_public = ! $this->database->is_public; @@ -120,10 +139,25 @@ class General extends Component public function refresh(): void { $this->database->refresh(); + $this->refreshView(); + } + + private function refreshView() + { + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; + $this->redis_version = $this->database->getRedisVersion(); + $this->redis_username = $this->database->redis_username; + $this->redis_password = $this->database->redis_password; } public function render() { return view('livewire.project.database.redis.general'); } + + public function isSharedVariable($name) + { + return $this->database->runtime_environment_variables()->where('key', $name)->where('is_shared', true)->exists(); + } } diff --git a/app/Livewire/Project/Database/ScheduledBackups.php b/app/Livewire/Project/Database/ScheduledBackups.php index beb5a9c39..412240bd4 100644 --- a/app/Livewire/Project/Database/ScheduledBackups.php +++ b/app/Livewire/Project/Database/ScheduledBackups.php @@ -26,10 +26,10 @@ class ScheduledBackups extends Component public function mount(): void { if ($this->selectedBackupId) { - $this->setSelectedBackup($this->selectedBackupId); + $this->setSelectedBackup($this->selectedBackupId, true); } $this->parameters = get_route_parameters(); - if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') { + if ($this->database->getMorphClass() === \App\Models\ServiceDatabase::class) { $this->type = 'service-database'; } else { $this->type = 'database'; @@ -37,10 +37,13 @@ class ScheduledBackups extends Component $this->s3s = currentTeam()->s3s; } - public function setSelectedBackup($backupId) + public function setSelectedBackup($backupId, $force = false) { + if ($this->selectedBackupId === $backupId && ! $force) { + return; + } $this->selectedBackupId = $backupId; - $this->selectedBackup = $this->database->scheduledBackups->find($this->selectedBackupId); + $this->selectedBackup = $this->database->scheduledBackups->find($backupId); if (is_null($this->selectedBackup)) { $this->selectedBackupId = null; } diff --git a/app/Livewire/Project/DeleteEnvironment.php b/app/Livewire/Project/DeleteEnvironment.php index 22478916f..1ee5de269 100644 --- a/app/Livewire/Project/DeleteEnvironment.php +++ b/app/Livewire/Project/DeleteEnvironment.php @@ -7,15 +7,22 @@ use Livewire\Component; class DeleteEnvironment extends Component { - public array $parameters; - public int $environment_id; public bool $disabled = false; + public string $environmentName = ''; + + public array $parameters; + public function mount() { - $this->parameters = get_route_parameters(); + try { + $this->environmentName = Environment::findOrFail($this->environment_id)->name; + $this->parameters = get_route_parameters(); + } catch (\Exception $e) { + return handleError($e, $this); + } } public function delete() @@ -27,9 +34,9 @@ class DeleteEnvironment extends Component if ($environment->isEmpty()) { $environment->delete(); - return redirect()->route('project.show', ['project_uuid' => $this->parameters['project_uuid']]); + return redirect()->route('project.show', parameters: ['project_uuid' => $this->parameters['project_uuid']]); } - return $this->dispatch('error', 'Environment has defined resources, please delete them first.'); + return $this->dispatch('error', "Environment {$environment->name} has defined resources, please delete them first."); } } diff --git a/app/Livewire/Project/DeleteProject.php b/app/Livewire/Project/DeleteProject.php index 499b86e3e..f320a19b0 100644 --- a/app/Livewire/Project/DeleteProject.php +++ b/app/Livewire/Project/DeleteProject.php @@ -13,9 +13,12 @@ class DeleteProject extends Component public bool $disabled = false; + public string $projectName = ''; + public function mount() { $this->parameters = get_route_parameters(); + $this->projectName = Project::findOrFail($this->project_id)->name; } public function delete() @@ -24,11 +27,12 @@ class DeleteProject extends Component 'project_id' => 'required|int', ]); $project = Project::findOrFail($this->project_id); - if ($project->applications->count() > 0) { - return $this->dispatch('error', 'Project has resources defined, please delete them first.'); - } - $project->delete(); + if ($project->isEmpty()) { + $project->delete(); - return redirect()->route('project.index'); + return redirect()->route('project.index'); + } + + return $this->dispatch('error', "Project {$project->name} has resources defined, please delete them first."); } } diff --git a/app/Livewire/Project/Edit.php b/app/Livewire/Project/Edit.php index bebec4752..463febb10 100644 --- a/app/Livewire/Project/Edit.php +++ b/app/Livewire/Project/Edit.php @@ -3,34 +3,47 @@ namespace App\Livewire\Project; use App\Models\Project; +use Livewire\Attributes\Validate; use Livewire\Component; class Edit extends Component { public Project $project; - protected $rules = [ - 'project.name' => 'required|min:3|max:255', - 'project.description' => 'nullable|string|max:255', - ]; + #[Validate(['required', 'string', 'min:3', 'max:255'])] + public string $name; - public function mount() + #[Validate(['nullable', 'string', 'max:255'])] + public ?string $description = null; + + public function mount(string $project_uuid) { - $projectUuid = request()->route('project_uuid'); - $teamId = currentTeam()->id; - $project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first(); - if (! $project) { - return redirect()->route('dashboard'); + try { + $this->project = Project::where('team_id', currentTeam()->id)->where('uuid', $project_uuid)->firstOrFail(); + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->project->update([ + 'name' => $this->name, + 'description' => $this->description, + ]); + } else { + $this->name = $this->project->name; + $this->description = $this->project->description; } - $this->project = $project; } public function submit() { try { - $this->validate(); - $this->project->save(); - $this->dispatch('saved'); + $this->syncData(true); $this->dispatch('success', 'Project updated.'); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Project/EnvironmentEdit.php b/app/Livewire/Project/EnvironmentEdit.php index 16fc7bc36..f48220b3d 100644 --- a/app/Livewire/Project/EnvironmentEdit.php +++ b/app/Livewire/Project/EnvironmentEdit.php @@ -4,6 +4,8 @@ namespace App\Livewire\Project; use App\Models\Application; use App\Models\Project; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class EnvironmentEdit extends Component @@ -12,29 +14,45 @@ class EnvironmentEdit extends Component public Application $application; + #[Locked] public $environment; - public array $parameters; + #[Validate(['required', 'string', 'min:3', 'max:255'])] + public string $name; - protected $rules = [ - 'environment.name' => 'required|min:3|max:255', - 'environment.description' => 'nullable|min:3|max:255', - ]; + #[Validate(['nullable', 'string', 'max:255'])] + public ?string $description = null; - public function mount() + public function mount(string $project_uuid, string $environment_name) { - $this->parameters = get_route_parameters(); - $this->project = Project::ownedByCurrentTeam()->where('uuid', request()->route('project_uuid'))->first(); - $this->environment = $this->project->environments()->where('name', request()->route('environment_name'))->first(); + try { + $this->project = Project::ownedByCurrentTeam()->where('uuid', $project_uuid)->firstOrFail(); + $this->environment = $this->project->environments()->where('name', $environment_name)->firstOrFail(); + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->environment->update([ + 'name' => $this->name, + 'description' => $this->description, + ]); + } else { + $this->name = $this->environment->name; + $this->description = $this->environment->description; + } } public function submit() { - $this->validate(); try { - $this->environment->save(); - - return redirect()->route('project.environment.edit', ['project_uuid' => $this->project->uuid, 'environment_name' => $this->environment->name]); + $this->syncData(true); + $this->redirectRoute('project.environment.edit', ['environment_name' => $this->environment->name, 'project_uuid' => $this->project->uuid]); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Index.php b/app/Livewire/Project/Index.php index 0e4f15a5c..f8eb838be 100644 --- a/app/Livewire/Project/Index.php +++ b/app/Livewire/Project/Index.php @@ -18,7 +18,11 @@ class Index extends Component public function mount() { $this->private_keys = PrivateKey::ownedByCurrentTeam()->get(); - $this->projects = Project::ownedByCurrentTeam()->get(); + $this->projects = Project::ownedByCurrentTeam()->get()->map(function ($project) { + $project->settingsRoute = route('project.edit', ['project_uuid' => $project->uuid]); + + return $project; + }); $this->servers = Server::ownedByCurrentTeam()->count(); } diff --git a/app/Livewire/Project/New/DockerCompose.php b/app/Livewire/Project/New/DockerCompose.php index 633ce5bda..199a20cf6 100644 --- a/app/Livewire/Project/New/DockerCompose.php +++ b/app/Livewire/Project/New/DockerCompose.php @@ -5,6 +5,8 @@ namespace App\Livewire\Project\New; use App\Models\EnvironmentVariable; use App\Models\Project; use App\Models\Service; +use App\Models\StandaloneDocker; +use App\Models\SwarmDocker; use Illuminate\Support\Str; use Livewire\Component; use Symfony\Component\Yaml\Yaml; @@ -58,12 +60,26 @@ class DockerCompose extends Component $project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); + + $destination_uuid = $this->query['destination']; + $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); + if (! $destination) { + $destination = SwarmDocker::where('uuid', $destination_uuid)->first(); + } + if (! $destination) { + throw new \Exception('Destination not found. What?!'); + } + $destination_class = $destination->getMorphClass(); + $service = Service::create([ 'name' => 'service'.Str::random(10), 'docker_compose_raw' => $this->dockerComposeRaw, 'environment_id' => $environment->id, 'server_id' => (int) $server_id, + 'destination_id' => $destination->id, + 'destination_type' => $destination_class, ]); + $variables = parseEnvFormatToArray($this->envFile); foreach ($variables as $key => $variable) { EnvironmentVariable::create([ diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index 65a98b37f..417fb2ea0 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -6,7 +6,6 @@ use App\Models\Application; use App\Models\Project; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; -use Illuminate\Support\Str; use Livewire\Component; use Visus\Cuid2\Cuid2; @@ -29,9 +28,9 @@ class DockerImage extends Component $this->validate([ 'dockerImage' => 'required', ]); - $image = Str::of($this->dockerImage)->before(':'); - if (Str::of($this->dockerImage)->contains(':')) { - $tag = Str::of($this->dockerImage)->after(':'); + $image = str($this->dockerImage)->before(':'); + if (str($this->dockerImage)->contains(':')) { + $tag = str($this->dockerImage)->after(':'); } else { $tag = 'latest'; } @@ -47,9 +46,8 @@ class DockerImage extends Component $project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); - ray($image, $tag); $application = Application::create([ - 'name' => 'docker-image-'.new Cuid2(7), + 'name' => 'docker-image-'.new Cuid2, 'repository_project_id' => 0, 'git_repository' => 'coollabsio/coolify', 'git_branch' => 'main', diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php index 76b337c01..2f4f5a25c 100644 --- a/app/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Livewire/Project/New/GithubPrivateRepository.php @@ -53,6 +53,12 @@ class GithubPrivateRepository extends Component public ?string $publish_directory = null; + // In case of docker compose + public ?string $base_directory = null; + + public ?string $docker_compose_location = '/docker-compose.yaml'; + // End of docker compose + protected int $page = 1; public $build_pack = 'nixpacks'; @@ -68,6 +74,16 @@ class GithubPrivateRepository extends Component $this->github_apps = GithubApp::private(); } + public function updatedBaseDirectory() + { + if ($this->base_directory) { + $this->base_directory = rtrim($this->base_directory, '/'); + if (! str($this->base_directory)->startsWith('/')) { + $this->base_directory = '/'.$this->base_directory; + } + } + } + public function updatedBuildPack() { if ($this->build_pack === 'nixpacks') { @@ -137,7 +153,6 @@ class GithubPrivateRepository extends Component protected function loadBranchByPage() { - ray('Loading page '.$this->page); $response = Http::withToken($this->token)->get("{$this->github_app->api_url}/repos/{$this->selected_repository_owner}/{$this->selected_repository_repo}/branches?per_page=100&page={$this->page}"); $json = $response->json(); if ($response->status() !== 200) { @@ -184,6 +199,10 @@ class GithubPrivateRepository extends Component if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') { $application->health_check_enabled = false; } + if ($this->build_pack === 'dockercompose') { + $application['docker_compose_location'] = $this->docker_compose_location; + $application['base_directory'] = $this->base_directory; + } $fqdn = generateFqdn($destination->server, $application->uuid); $application->fqdn = $fqdn; diff --git a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php index 690149cc4..b46c4a794 100644 --- a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php +++ b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php @@ -33,6 +33,12 @@ class GithubPrivateRepositoryDeployKey extends Component public ?string $publish_directory = null; + // In case of docker compose + public ?string $base_directory = null; + + public ?string $docker_compose_location = '/docker-compose.yaml'; + // End of docker compose + public string $repository_url; public string $branch; @@ -163,6 +169,10 @@ class GithubPrivateRepositoryDeployKey extends Component if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') { $application_init['health_check_enabled'] = false; } + if ($this->build_pack === 'dockercompose') { + $application_init['docker_compose_location'] = $this->docker_compose_location; + $application_init['base_directory'] = $this->base_directory; + } $application = Application::create($application_init); $application->settings->is_static = $this->is_static; $application->settings->save(); @@ -188,12 +198,12 @@ class GithubPrivateRepositoryDeployKey extends Component $this->git_host = $this->repository_url_parsed->getHost(); $this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2); - if ($this->git_host == 'github.com') { + if ($this->git_host === 'github.com') { $this->git_source = GithubApp::where('name', 'Public GitHub')->first(); return; } - if (Str::of($this->repository_url)->startsWith('http')) { + if (str($this->repository_url)->startsWith('http')) { $this->git_host = $this->repository_url_parsed->getHost(); $this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2); $this->git_repository = Str::finish("git@$this->git_host:$this->git_repository", '.git'); diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index 739061f1f..bd35dccef 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -25,14 +25,22 @@ class PublicGitRepository extends Component public $query; - public bool $branch_found = false; + public bool $branchFound = false; - public string $selected_branch = 'main'; + public string $selectedBranch = 'main'; - public bool $is_static = false; + public bool $isStatic = false; + + public bool $checkCoolifyConfig = true; public ?string $publish_directory = null; + // In case of docker compose + public string $base_directory = '/'; + + public ?string $docker_compose_location = '/docker-compose.yaml'; + // End of docker compose + public string $git_branch = 'main'; public int $rate_limit_remaining = 0; @@ -56,17 +64,21 @@ class PublicGitRepository extends Component protected $rules = [ 'repository_url' => 'required|url', 'port' => 'required|numeric', - 'is_static' => 'required|boolean', + 'isStatic' => 'required|boolean', 'publish_directory' => 'nullable|string', 'build_pack' => 'required|string', + 'base_directory' => 'nullable|string', + 'docker_compose_location' => 'nullable|string', ]; protected $validationAttributes = [ 'repository_url' => 'repository', 'port' => 'port', - 'is_static' => 'static', + 'isStatic' => 'static', 'publish_directory' => 'publish directory', 'build_pack' => 'build pack', + 'base_directory' => 'base directory', + 'docker_compose_location' => 'docker compose location', ]; public function mount() @@ -79,6 +91,26 @@ class PublicGitRepository extends Component $this->query = request()->query(); } + public function updatedBaseDirectory() + { + if ($this->base_directory) { + $this->base_directory = rtrim($this->base_directory, '/'); + if (! str($this->base_directory)->startsWith('/')) { + $this->base_directory = '/'.$this->base_directory; + } + } + } + + public function updatedDockerComposeLocation() + { + if ($this->docker_compose_location) { + $this->docker_compose_location = rtrim($this->docker_compose_location, '/'); + if (! str($this->docker_compose_location)->startsWith('/')) { + $this->docker_compose_location = '/'.$this->docker_compose_location; + } + } + } + public function updatedBuildPack() { if ($this->build_pack === 'nixpacks') { @@ -86,17 +118,17 @@ class PublicGitRepository extends Component $this->port = 3000; } elseif ($this->build_pack === 'static') { $this->show_is_static = false; - $this->is_static = false; + $this->isStatic = false; $this->port = 80; } else { $this->show_is_static = false; - $this->is_static = false; + $this->isStatic = false; } } public function instantSave() { - if ($this->is_static) { + if ($this->isStatic) { $this->port = 80; $this->publish_directory = '/dist'; } else { @@ -106,12 +138,7 @@ class PublicGitRepository extends Component $this->dispatch('success', 'Application settings updated!'); } - public function load_any_git() - { - $this->branch_found = true; - } - - public function load_branch() + public function loadBranch() { try { if (str($this->repository_url)->startsWith('git@')) { @@ -128,23 +155,28 @@ class PublicGitRepository extends Component ) { $this->repository_url = $this->repository_url.'.git'; } - if (str($this->repository_url)->contains('github.com')) { - $this->repository_url = str($this->repository_url)->before('.git')->value(); + if (str($this->repository_url)->contains('github.com') && str($this->repository_url)->endsWith('.git')) { + $this->repository_url = str($this->repository_url)->beforeLast('.git')->value(); } } catch (\Throwable $e) { return handleError($e, $this); } try { - $this->branch_found = false; - $this->get_git_source(); - $this->get_branch(); - $this->selected_branch = $this->git_branch; + $this->branchFound = false; + $this->getGitSource(); + $this->getBranch(); + $this->selectedBranch = $this->git_branch; } catch (\Throwable $e) { - ray($e->getMessage()); - if (! $this->branch_found && $this->git_branch == 'main') { + if ($this->rate_limit_remaining == 0) { + $this->selectedBranch = $this->git_branch; + $this->branchFound = true; + + return; + } + if (! $this->branchFound && $this->git_branch === 'main') { try { $this->git_branch = 'master'; - $this->get_branch(); + $this->getBranch(); } catch (\Throwable $e) { return handleError($e, $this); } @@ -154,14 +186,17 @@ class PublicGitRepository extends Component } } - private function get_git_source() + private function getGitSource() { $this->repository_url_parsed = Url::fromString($this->repository_url); $this->git_host = $this->repository_url_parsed->getHost(); $this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2); - $this->git_branch = $this->repository_url_parsed->getSegment(4) ?? 'main'; - - if ($this->git_host == 'github.com') { + if ($this->repository_url_parsed->getSegment(3) === 'tree') { + $this->git_branch = str($this->repository_url_parsed->getPath())->after('tree/')->value(); + } else { + $this->git_branch = 'main'; + } + if ($this->git_host === 'github.com') { $this->git_source = GithubApp::where('name', 'Public GitHub')->first(); return; @@ -170,17 +205,17 @@ class PublicGitRepository extends Component $this->git_source = 'other'; } - private function get_branch() + private function getBranch() { if ($this->git_source === 'other') { - $this->branch_found = true; + $this->branchFound = true; return; } - if ($this->git_source->getMorphClass() === 'App\Models\GithubApp') { + if ($this->git_source->getMorphClass() === \App\Models\GithubApp::class) { ['rate_limit_remaining' => $this->rate_limit_remaining, 'rate_limit_reset' => $this->rate_limit_reset] = githubApi(source: $this->git_source, endpoint: "/repos/{$this->git_repository}/branches/{$this->git_branch}"); $this->rate_limit_reset = Carbon::parse((int) $this->rate_limit_reset)->format('Y-M-d H:i:s'); - $this->branch_found = true; + $this->branchFound = true; } } @@ -242,6 +277,7 @@ class PublicGitRepository extends Component 'destination_id' => $destination->id, 'destination_type' => $destination_class, 'build_pack' => $this->build_pack, + 'base_directory' => $this->base_directory, ]; } else { $application_init = [ @@ -256,20 +292,30 @@ class PublicGitRepository extends Component 'source_id' => $this->git_source->id, 'source_type' => $this->git_source->getMorphClass(), 'build_pack' => $this->build_pack, + 'base_directory' => $this->base_directory, ]; } if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') { $application_init['health_check_enabled'] = false; } + if ($this->build_pack === 'dockercompose') { + $application_init['docker_compose_location'] = $this->docker_compose_location; + $application_init['base_directory'] = $this->base_directory; + } $application = Application::create($application_init); - $application->settings->is_static = $this->is_static; + $application->settings->is_static = $this->isStatic; $application->settings->save(); - $fqdn = generateFqdn($destination->server, $application->uuid); $application->fqdn = $fqdn; $application->save(); + if ($this->checkCoolifyConfig) { + // $config = loadConfigFromGit($this->repository_url, $this->git_branch, $this->base_directory, $this->query['server_id'], auth()->user()->currentTeam()->id); + // if ($config) { + // $application->setConfig($config); + // } + } return redirect()->route('project.application.configuration', [ 'application_uuid' => $application->uuid, diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php index b8d186dab..2dc9abbf1 100644 --- a/app/Livewire/Project/New/Select.php +++ b/app/Livewire/Project/New/Select.php @@ -45,13 +45,12 @@ class Select extends Component public ?string $selectedEnvironment = null; - public ?string $existingPostgresqlUrl = null; + public string $postgresql_type = 'postgres:16-alpine'; - public ?string $search = null; + public ?string $existingPostgresqlUrl = null; protected $queryString = [ 'server_id', - 'search', ]; public function mount() @@ -88,40 +87,119 @@ class Select extends Component // } // } - public function updatedSearch() + public function loadServices() { - $this->loadServices(); - } + $services = get_service_templates(true); + $services = collect($services)->map(function ($service, $key) { + return [ + 'name' => str($key)->headline(), + 'logo' => asset(data_get($service, 'logo', 'svgs/coolify.png')), + ] + (array) $service; + })->all(); + $gitBasedApplications = [ + [ + 'id' => 'public', + 'name' => 'Public Repository', + 'description' => 'You can deploy any kind of public repositories from the supported git providers.', + 'logo' => asset('svgs/git.svg'), + ], + [ + 'id' => 'private-gh-app', + 'name' => 'Private Repository (with GitHub App)', + 'description' => 'You can deploy public & private repositories through your GitHub Apps.', + 'logo' => asset('svgs/github.svg'), + ], + [ + 'id' => 'private-deploy-key', + 'name' => 'Private Repository (with Deploy Key)', + 'description' => 'You can deploy private repositories with a deploy key.', + 'logo' => asset('svgs/git.svg'), + ], + ]; + $dockerBasedApplications = [ + [ + 'id' => 'dockerfile', + 'name' => 'Dockerfile', + 'description' => 'You can deploy a simple Dockerfile, without Git.', + 'logo' => asset('svgs/docker.svg'), + ], + [ + 'id' => 'docker-compose-empty', + 'name' => 'Docker Compose Empty', + 'description' => 'You can deploy complex application easily with Docker Compose, without Git.', + 'logo' => asset('svgs/docker.svg'), + ], + [ + 'id' => 'docker-image', + 'name' => 'Docker Image', + 'description' => 'You can deploy an existing Docker Image from any Registry, without Git.', + 'logo' => asset('svgs/docker.svg'), + ], + ]; + $databases = [ + [ + 'id' => 'postgresql', + 'name' => 'PostgreSQL', + 'description' => 'PostgreSQL is an object-relational database known for its robustness, advanced features, and strong standards compliance.', + 'logo' => ' +', + ], + [ + 'id' => 'mysql', + 'name' => 'MySQL', + 'description' => 'MySQL is an open-source relational database management system. ', + 'logo' => ' + + + +', - public function loadServices(bool $force = false) - { - try { - $this->loadingServices = true; - if (count($this->allServices) > 0 && ! $force) { - if (! $this->search) { - $this->services = $this->allServices; + ], + [ + 'id' => 'mariadb', + 'name' => 'MariaDB', + 'description' => 'MariaDB is a community-developed, commercially supported fork of the MySQL relational database management system, intended to remain free and open-source.', + 'logo' => '', + ], + [ + 'id' => 'redis', + 'name' => 'Redis', + 'description' => 'Redis is a source-available, in-memory storage, used as a distributed, in-memory key–value database, cache and message broker, with optional durability.', + 'logo' => '', + ], + [ + 'id' => 'keydb', + 'name' => 'KeyDB', + 'description' => 'KeyDB is a database that offers high performance, low latency, and scalability for various data structures and workloads.', + 'logo' => '
', + ], + [ + 'id' => 'dragonfly', + 'name' => 'Dragonfly', + 'description' => 'Dragonfly DB is a drop-in Redis replacement that delivers 25x more throughput and 12x faster snapshotting than Redis.', + 'logo' => '
', + ], + [ + 'id' => 'mongodb', + 'name' => 'MongoDB', + 'description' => 'MongoDB is a source-available, cross-platform, document-oriented database program.', + 'logo' => '', + ], + [ + 'id' => 'clickhouse', + 'name' => 'ClickHouse', + 'description' => 'ClickHouse is a column-oriented database that supports real-time analytics, business intelligence, observability, ML and GenAI, and more.', + 'logo' => '
', + ], - return; - } - $this->services = $this->allServices->filter(function ($service, $key) { - $tags = collect(data_get($service, 'tags', [])); + ]; - return str_contains(strtolower($key), strtolower($this->search)) || $tags->contains(function ($tag) { - return str_contains(strtolower($tag), strtolower($this->search)); - }); - }); - } else { - $this->search = null; - $this->allServices = get_service_templates($force); - $this->services = $this->allServices->filter(function ($service, $key) { - return str_contains(strtolower($key), strtolower($this->search)); - }); - } - } catch (\Throwable $e) { - return handleError($e, $this); - } finally { - $this->loadingServices = false; - } + return [ + 'services' => $services, + 'gitBasedApplications' => $gitBasedApplications, + 'dockerBasedApplications' => $dockerBasedApplications, + 'databases' => $databases, + ]; } public function instantSave() @@ -139,6 +217,7 @@ class Select extends Component public function setType(string $type) { + $type = str($type)->lower()->slug()->value(); if ($this->loading) { return; } @@ -176,10 +255,12 @@ class Select extends Component return; } - // if (count($this->servers) === 1) { - // $server = $this->servers->first(); - // $this->setServer($server); - // } + if (count($this->servers) === 1) { + $server = $this->servers->first(); + if ($server instanceof Server) { + $this->setServer($server); + } + } if (! is_null($this->server)) { $foundServer = $this->servers->where('id', $this->server->id)->first(); if ($foundServer) { @@ -195,6 +276,15 @@ class Select extends Component $this->server = $server; $this->standaloneDockers = $server->standaloneDockers; $this->swarmDockers = $server->swarmDockers; + $count = count($this->standaloneDockers) + count($this->swarmDockers); + if ($count === 1) { + $docker = $this->standaloneDockers->first() ?? $this->swarmDockers->first(); + if ($docker) { + $this->setDestination($docker->uuid); + + return $this->whatToDoNext(); + } + } $this->current_step = 'destinations'; } @@ -202,18 +292,41 @@ class Select extends Component { $this->destination_uuid = $destination_uuid; + return $this->whatToDoNext(); + } + + public function setPostgresqlType(string $type) + { + $this->postgresql_type = $type; + return redirect()->route('project.resource.create', [ 'project_uuid' => $this->parameters['project_uuid'], 'environment_name' => $this->parameters['environment_name'], 'type' => $this->type, 'destination' => $this->destination_uuid, 'server_id' => $this->server_id, + 'database_image' => $this->postgresql_type, ]); } + public function whatToDoNext() + { + if ($this->type === 'postgresql') { + $this->current_step = 'select-postgresql-type'; + } else { + return redirect()->route('project.resource.create', [ + 'project_uuid' => $this->parameters['project_uuid'], + 'environment_name' => $this->parameters['environment_name'], + 'type' => $this->type, + 'destination' => $this->destination_uuid, + 'server_id' => $this->server_id, + ]); + } + } + public function loadServers() { - $this->servers = Server::isUsable()->get(); + $this->servers = Server::isUsable()->get()->sortBy('name'); $this->allServers = $this->servers; } } diff --git a/app/Livewire/Project/New/SimpleDockerfile.php b/app/Livewire/Project/New/SimpleDockerfile.php index 6f6bc9185..3c7f42329 100644 --- a/app/Livewire/Project/New/SimpleDockerfile.php +++ b/app/Livewire/Project/New/SimpleDockerfile.php @@ -53,7 +53,7 @@ CMD ["nginx", "-g", "daemon off;"] $port = 80; } $application = Application::create([ - 'name' => 'dockerfile-'.new Cuid2(7), + 'name' => 'dockerfile-'.new Cuid2, 'repository_project_id' => 0, 'git_repository' => 'coollabsio/coolify', 'git_branch' => 'main', diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php index 341dd93d8..9266a57fc 100644 --- a/app/Livewire/Project/Resource/Create.php +++ b/app/Livewire/Project/Resource/Create.php @@ -18,6 +18,7 @@ class Create extends Component $type = str(request()->query('type')); $destination_uuid = request()->query('destination'); $server_id = request()->query('server_id'); + $database_image = request()->query('database_image'); $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); if (! $project) { @@ -33,7 +34,11 @@ class Create extends Component if (in_array($type, DATABASE_TYPES)) { if ($type->value() === 'postgresql') { - $database = create_standalone_postgresql($environment->id, $destination_uuid); + $database = create_standalone_postgresql( + environmentId: $environment->id, + destinationUuid: $destination_uuid, + databaseImage: $database_image + ); } elseif ($type->value() === 'redis') { $database = create_standalone_redis($environment->id, $destination_uuid); } elseif ($type->value() === 'mongodb') { @@ -86,18 +91,15 @@ class Create extends Component $oneClickDotEnvs->each(function ($value) use ($service) { $key = str()->before($value, '='); $value = str(str()->after($value, '=')); - $generatedValue = $value; - if ($value->contains('SERVICE_')) { - $command = $value->after('SERVICE_')->beforeLast('_'); - $generatedValue = generateEnvValue($command->value(), $service); + if ($value) { + EnvironmentVariable::create([ + 'key' => $key, + 'value' => $value, + 'service_id' => $service->id, + 'is_build_time' => false, + 'is_preview' => false, + ]); } - EnvironmentVariable::create([ - 'key' => $key, - 'value' => $generatedValue, - 'service_id' => $service->id, - 'is_build_time' => false, - 'is_preview' => false, - ]); }); } $service->parse(isNew: true); diff --git a/app/Livewire/Project/Resource/EnvironmentSelect.php b/app/Livewire/Project/Resource/EnvironmentSelect.php new file mode 100644 index 000000000..efb1b6ca2 --- /dev/null +++ b/app/Livewire/Project/Resource/EnvironmentSelect.php @@ -0,0 +1,35 @@ +selectedEnvironment = request()->route('environment_name'); + $this->project_uuid = request()->route('project_uuid'); + } + + public function updatedSelectedEnvironment($value) + { + if ($value === 'edit') { + return redirect()->route('project.show', [ + 'project_uuid' => $this->project_uuid, + ]); + } else { + return redirect()->route('project.resource.index', [ + 'project_uuid' => $this->project_uuid, + 'environment_name' => $value, + ]); + } + } +} diff --git a/app/Livewire/Project/Resource/Index.php b/app/Livewire/Project/Resource/Index.php index 71ce2c356..283496887 100644 --- a/app/Livewire/Project/Resource/Index.php +++ b/app/Livewire/Project/Resource/Index.php @@ -32,8 +32,11 @@ class Index extends Component public $services = []; + public array $parameters; + public function mount() { + $this->parameters = get_route_parameters(); $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); if (! $project) { return redirect()->route('dashboard'); @@ -44,7 +47,6 @@ class Index extends Component } $this->project = $project; $this->environment = $environment; - $this->applications = $this->environment->applications->load(['tags']); $this->applications = $this->applications->map(function ($application) { if (data_get($application, 'environment.project.uuid')) { diff --git a/app/Livewire/Project/Service/Configuration.php b/app/Livewire/Project/Service/Configuration.php index 47534ded1..319ead361 100644 --- a/app/Livewire/Project/Service/Configuration.php +++ b/app/Livewire/Project/Service/Configuration.php @@ -4,6 +4,7 @@ namespace App\Livewire\Project\Service; use App\Actions\Docker\GetContainersStatus; use App\Models\Service; +use Illuminate\Support\Facades\Auth; use Livewire\Component; class Configuration extends Component @@ -20,7 +21,7 @@ class Configuration extends Component public function getListeners() { - $userId = auth()->user()->id; + $userId = Auth::id(); return [ "echo-private:user.{$userId},ServiceStatusChanged" => 'check_status', @@ -52,7 +53,7 @@ class Configuration extends Component $application = $this->service->applications->find($id); if ($application) { $application->restart(); - $this->dispatch('success', 'Application restarted successfully.'); + $this->dispatch('success', 'Service application restarted successfully.'); } } catch (\Exception $e) { return handleError($e, $this); @@ -65,7 +66,7 @@ class Configuration extends Component $database = $this->service->databases->find($id); if ($database) { $database->restart(); - $this->dispatch('success', 'Database restarted successfully.'); + $this->dispatch('success', 'Service database restarted successfully.'); } } catch (\Exception $e) { return handleError($e, $this); @@ -76,8 +77,13 @@ class Configuration extends Component { try { GetContainersStatus::run($this->service->server); - // dispatch_sync(new ContainerStatusJob($this->service->server)); - $this->dispatch('refresh')->self(); + $this->service->applications->each(function ($application) { + $application->refresh(); + }); + $this->service->databases->each(function ($database) { + $database->refresh(); + }); + $this->dispatch('$refresh'); } catch (\Exception $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Service/Database.php b/app/Livewire/Project/Service/Database.php index 9804fb5ba..9f02db05c 100644 --- a/app/Livewire/Project/Service/Database.php +++ b/app/Livewire/Project/Service/Database.php @@ -95,8 +95,7 @@ class Database extends Component $this->database->save(); updateCompose($this->database); $this->dispatch('success', 'Database saved.'); - } catch (\Throwable $e) { - ray($e); + } catch (\Throwable) { } finally { $this->dispatch('generateDockerCompose'); } diff --git a/app/Livewire/Project/Service/EditCompose.php b/app/Livewire/Project/Service/EditCompose.php index fd4d684b1..dc043e65a 100644 --- a/app/Livewire/Project/Service/EditCompose.php +++ b/app/Livewire/Project/Service/EditCompose.php @@ -11,12 +11,29 @@ class EditCompose extends Component public $serviceId; + protected $listeners = [ + 'refreshEnvs', + 'envsUpdated', + 'refresh' => 'envsUpdated', + ]; + protected $rules = [ 'service.docker_compose_raw' => 'required', 'service.docker_compose' => 'required', 'service.is_container_label_escape_enabled' => 'required', ]; + public function envsUpdated() + { + $this->dispatch('saveCompose', $this->service->docker_compose_raw); + $this->refreshEnvs(); + } + + public function refreshEnvs() + { + $this->service = Service::find($this->serviceId); + } + public function mount() { $this->service = Service::find($this->serviceId); @@ -26,6 +43,7 @@ class EditCompose extends Component { $this->dispatch('info', 'Saving new docker compose...'); $this->dispatch('saveCompose', $this->service->docker_compose_raw); + $this->dispatch('refreshStorages'); } public function instantSave() diff --git a/app/Livewire/Project/Service/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php index 70e8006c7..e89aeda85 100644 --- a/app/Livewire/Project/Service/EditDomain.php +++ b/app/Livewire/Project/Service/EditDomain.php @@ -4,6 +4,7 @@ namespace App\Livewire\Project\Service; use App\Models\ServiceApplication; use Livewire\Component; +use Spatie\Url\Url; class EditDomain extends Component { @@ -21,20 +22,21 @@ class EditDomain extends Component $this->application = ServiceApplication::find($this->applicationId); } - public function updatedApplicationFqdn() - { - $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { - return str($domain)->trim()->lower(); - }); - $this->application->fqdn = $this->application->fqdn->unique()->implode(','); - $this->application->save(); - } - public function submit() { try { + $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); + $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); + $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { + Url::fromString($domain, ['http', 'https']); + + return str($domain)->trim()->lower(); + }); + $this->application->fqdn = $this->application->fqdn->unique()->implode(','); + $warning = sslipDomainWarning($this->application->fqdn); + if ($warning) { + $this->dispatch('warning', __('warning.sslipdomain')); + } check_domain_usage(resource: $this->application); $this->validate(); $this->application->save(); @@ -42,14 +44,18 @@ class EditDomain extends Component if (str($this->application->fqdn)->contains(',')) { $this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.

Only use multiple domains if you know what you are doing.'); } else { - $this->dispatch('success', 'Service saved.'); + ! $warning && $this->dispatch('success', 'Service saved.'); } - } catch (\Throwable $e) { - return handleError($e, $this); - } finally { $this->application->service->parse(); $this->dispatch('refresh'); $this->dispatch('configurationChanged'); + } catch (\Throwable $e) { + $originalFqdn = $this->application->getOriginal('fqdn'); + if ($originalFqdn !== $this->application->fqdn) { + $this->application->fqdn = $originalFqdn; + } + + return handleError($e, $this); } } diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php index 201ebf58f..4d070bc0c 100644 --- a/app/Livewire/Project/Service/FileStorage.php +++ b/app/Livewire/Project/Service/FileStorage.php @@ -3,6 +3,7 @@ namespace App\Livewire\Project\Service; use App\Models\Application; +use App\Models\InstanceSettings; use App\Models\LocalFileVolume; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; @@ -14,7 +15,8 @@ use App\Models\StandaloneMongodb; use App\Models\StandaloneMysql; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; -use Illuminate\Support\Str; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; use Livewire\Component; class FileStorage extends Component @@ -27,23 +29,27 @@ class FileStorage extends Component public ?string $workdir = null; + public bool $permanently_delete = true; + protected $rules = [ 'fileStorage.is_directory' => 'required', 'fileStorage.fs_path' => 'required', 'fileStorage.mount_path' => 'required', 'fileStorage.content' => 'nullable', + 'fileStorage.is_based_on_git' => 'required|boolean', ]; public function mount() { $this->resource = $this->fileStorage->service; - if (Str::of($this->fileStorage->fs_path)->startsWith('.')) { + if (str($this->fileStorage->fs_path)->startsWith('.')) { $this->workdir = $this->resource->service?->workdir(); - $this->fs_path = Str::of($this->fileStorage->fs_path)->after('.'); + $this->fs_path = str($this->fileStorage->fs_path)->after('.'); } else { $this->workdir = null; $this->fs_path = $this->fileStorage->fs_path; } + $this->fileStorage->loadStorageOnServer(); } public function convertToDirectory() @@ -52,12 +58,13 @@ class FileStorage extends Component $this->fileStorage->deleteStorageOnServer(); $this->fileStorage->is_directory = true; $this->fileStorage->content = null; + $this->fileStorage->is_based_on_git = false; $this->fileStorage->save(); $this->fileStorage->saveStorageOnServer(); } catch (\Throwable $e) { return handleError($e, $this); } finally { - $this->dispatch('refresh_storages'); + $this->dispatch('refreshStorages'); } } @@ -67,25 +74,43 @@ class FileStorage extends Component $this->fileStorage->deleteStorageOnServer(); $this->fileStorage->is_directory = false; $this->fileStorage->content = null; + if (data_get($this->resource, 'settings.is_preserve_repository_enabled')) { + $this->fileStorage->is_based_on_git = true; + } $this->fileStorage->save(); $this->fileStorage->saveStorageOnServer(); } catch (\Throwable $e) { return handleError($e, $this); } finally { - $this->dispatch('refresh_storages'); + $this->dispatch('refreshStorages'); } } - public function delete() + public function delete($password) { + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); + + return; + } + } + try { - $this->fileStorage->deleteStorageOnServer(); + $message = 'File deleted.'; + if ($this->fileStorage->is_directory) { + $message = 'Directory deleted.'; + } + if ($this->permanently_delete) { + $message = 'Directory deleted from the server.'; + $this->fileStorage->deleteStorageOnServer(); + } $this->fileStorage->delete(); - $this->dispatch('success', 'File deleted.'); + $this->dispatch('success', $message); } catch (\Throwable $e) { return handleError($e, $this); } finally { - $this->dispatch('refresh_storages'); + $this->dispatch('refreshStorages'); } } @@ -115,6 +140,13 @@ class FileStorage extends Component public function render() { - return view('livewire.project.service.file-storage'); + return view('livewire.project.service.file-storage', [ + 'directoryDeletionCheckboxes' => [ + ['id' => 'permanently_delete', 'label' => 'The selected directory and all its contents will be permantely deleted form the server.'], + ], + 'fileDeletionCheckboxes' => [ + ['id' => 'permanently_delete', 'label' => 'The selected file will be permanently deleted form the server.'], + ], + ]); } } diff --git a/app/Livewire/Project/Service/Index.php b/app/Livewire/Project/Service/Index.php index 0a7b6ec90..ba4ebe2fc 100644 --- a/app/Livewire/Project/Service/Index.php +++ b/app/Livewire/Project/Service/Index.php @@ -48,7 +48,6 @@ class Index extends Component } catch (\Throwable $e) { return handleError($e, $this); } - } public function generateDockerCompose() diff --git a/app/Livewire/Project/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php index 7d3987b3d..ee43dc911 100644 --- a/app/Livewire/Project/Service/Navbar.php +++ b/app/Livewire/Project/Service/Navbar.php @@ -7,6 +7,7 @@ use App\Actions\Service\StopService; use App\Actions\Shared\PullImage; use App\Events\ServiceStatusChanged; use App\Models\Service; +use Illuminate\Support\Facades\Auth; use Livewire\Component; use Spatie\Activitylog\Models\Activity; @@ -20,10 +21,13 @@ class Navbar extends Component public $isDeploymentProgress = false; + public $docker_cleanup = true; + + public $title = 'Configuration'; + public function mount() { if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) { - ray('isConfigurationChanged init'); $this->service->isConfigurationChanged(true); $this->dispatch('configurationChanged'); } @@ -31,16 +35,17 @@ class Navbar extends Component public function getListeners() { - $userId = auth()->user()->id; + $userId = Auth::id(); return [ "echo-private:user.{$userId},ServiceStatusChanged" => 'serviceStarted', + 'envsUpdated' => '$refresh', ]; } public function serviceStarted() { - $this->dispatch('success', 'Service status changed.'); + // $this->dispatch('success', 'Service status changed.'); if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) { $this->service->isConfigurationChanged(true); $this->dispatch('configurationChanged'); @@ -49,24 +54,30 @@ class Navbar extends Component } } + public function check_status_without_notification() + { + $this->dispatch('check_status'); + } + public function check_status() { $this->dispatch('check_status'); $this->dispatch('success', 'Service status updated.'); } - public function render() - { - return view('livewire.project.service.navbar'); - } - public function checkDeployments() { - $activity = Activity::where('properties->type_uuid', $this->service->uuid)->latest()->first(); - $status = data_get($activity, 'properties.status'); - if ($status === 'queued' || $status === 'in_progress') { - $this->isDeploymentProgress = true; - } else { + try { + // TODO: This is a temporary solution. We need to refactor this. + // We need to delete null bytes somehow. + $activity = Activity::where('properties->type_uuid', $this->service->uuid)->latest()->first(); + $status = data_get($activity, 'properties.status'); + if ($status === 'queued' || $status === 'in_progress') { + $this->isDeploymentProgress = true; + } else { + $this->isDeploymentProgress = false; + } + } catch (\Throwable) { $this->isDeploymentProgress = false; } } @@ -84,14 +95,9 @@ class Navbar extends Component $this->dispatch('activityMonitor', $activity->id); } - public function stop(bool $forceCleanup = false) + public function stop() { - StopService::run($this->service); - if ($forceCleanup) { - $this->dispatch('success', 'Containers cleaned up.'); - } else { - $this->dispatch('success', 'Service stopped.'); - } + StopService::run($this->service, false, $this->docker_cleanup); ServiceStatusChanged::dispatch(); } @@ -103,11 +109,35 @@ class Navbar extends Component return; } - PullImage::run($this->service); - StopService::run($this->service); + StopService::run(service: $this->service, dockerCleanup: false); $this->service->parse(); $this->dispatch('imagePulled'); $activity = StartService::run($this->service); $this->dispatch('activityMonitor', $activity->id); } + + public function pullAndRestartEvent() + { + $this->checkDeployments(); + if ($this->isDeploymentProgress) { + $this->dispatch('error', 'There is a deployment in progress.'); + + return; + } + PullImage::run($this->service); + StopService::run(service: $this->service, dockerCleanup: false); + $this->service->parse(); + $this->dispatch('imagePulled'); + $activity = StartService::run($this->service); + $this->dispatch('activityMonitor', $activity->id); + } + + public function render() + { + return view('livewire.project.service.navbar', [ + 'checkboxes' => [ + ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')], + ], + ]); + } } diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php index e7d00c3dd..8324ee645 100644 --- a/app/Livewire/Project/Service/ServiceApplicationView.php +++ b/app/Livewire/Project/Service/ServiceApplicationView.php @@ -2,8 +2,12 @@ namespace App\Livewire\Project\Service; +use App\Models\InstanceSettings; use App\Models\ServiceApplication; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; use Livewire\Component; +use Spatie\Url\Url; class ServiceApplicationView extends Component { @@ -11,6 +15,10 @@ class ServiceApplicationView extends Component public $parameters; + public $docker_cleanup = true; + + public $delete_volumes = true; + protected $rules = [ 'application.human_name' => 'nullable', 'application.description' => 'nullable', @@ -23,22 +31,6 @@ class ServiceApplicationView extends Component 'application.is_stripprefix_enabled' => 'nullable|boolean', ]; - public function render() - { - return view('livewire.project.service.service-application-view'); - } - - public function updatedApplicationFqdn() - { - $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { - return str($domain)->trim()->lower(); - }); - $this->application->fqdn = $this->application->fqdn->unique()->implode(','); - $this->application->save(); - } - public function instantSave() { $this->submit(); @@ -56,8 +48,16 @@ class ServiceApplicationView extends Component $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); } - public function delete() + public function delete($password) { + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); + + return; + } + } + try { $this->application->delete(); $this->dispatch('success', 'Application deleted.'); @@ -76,6 +76,18 @@ class ServiceApplicationView extends Component public function submit() { try { + $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); + $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); + $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { + Url::fromString($domain, ['http', 'https']); + + return str($domain)->trim()->lower(); + }); + $this->application->fqdn = $this->application->fqdn->unique()->implode(','); + $warning = sslipDomainWarning($this->application->fqdn); + if ($warning) { + $this->dispatch('warning', __('warning.sslipdomain')); + } check_domain_usage(resource: $this->application); $this->validate(); $this->application->save(); @@ -83,12 +95,29 @@ class ServiceApplicationView extends Component if (str($this->application->fqdn)->contains(',')) { $this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.

Only use multiple domains if you know what you are doing.'); } else { - $this->dispatch('success', 'Service saved.'); + ! $warning && $this->dispatch('success', 'Service saved.'); } - } catch (\Throwable $e) { - return handleError($e, $this); - } finally { $this->dispatch('generateDockerCompose'); + } catch (\Throwable $e) { + $originalFqdn = $this->application->getOriginal('fqdn'); + if ($originalFqdn !== $this->application->fqdn) { + $this->application->fqdn = $originalFqdn; + } + + return handleError($e, $this); } } + + public function render() + { + return view('livewire.project.service.service-application-view', [ + 'checkboxes' => [ + ['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')], + ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')], + // ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'], + // ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'], + // ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.'] + ], + ]); + } } diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php index 05917f895..2c751aa92 100644 --- a/app/Livewire/Project/Service/StackForm.php +++ b/app/Livewire/Project/Service/StackForm.php @@ -33,7 +33,8 @@ class StackForm extends Component $key = data_get($field, 'key'); $value = data_get($field, 'value'); $rules = data_get($field, 'rules', 'nullable'); - $isPassword = data_get($field, 'isPassword'); + $isPassword = data_get($field, 'isPassword', false); + $customHelper = data_get($field, 'customHelper', false); $this->fields->put($key, [ 'serviceName' => $serviceName, 'key' => $key, @@ -41,19 +42,28 @@ class StackForm extends Component 'value' => $value, 'isPassword' => $isPassword, 'rules' => $rules, + 'customHelper' => $customHelper, ]); $this->rules["fields.$key.value"] = $rules; $this->validationAttributes["fields.$key.value"] = $fieldKey; } } - $this->fields = $this->fields->sortBy('name'); + $this->fields = $this->fields->groupBy('serviceName')->map(function ($group) { + return $group->sortBy(function ($field) { + return data_get($field, 'isPassword') ? 1 : 0; + })->mapWithKeys(function ($field) { + return [$field['key'] => $field]; + }); + })->flatMap(function ($group) { + return $group; + }); } public function saveCompose($raw) { $this->service->docker_compose_raw = $raw; - $this->submit(); + $this->submit(notify: false); } public function instantSave() @@ -62,7 +72,7 @@ class StackForm extends Component $this->dispatch('success', 'Service settings saved.'); } - public function submit() + public function submit($notify = true) { try { $this->validate(); @@ -75,14 +85,12 @@ class StackForm extends Component $this->service->parse(); $this->service->refresh(); $this->service->saveComposeConfigs(); - $this->dispatch('refreshStacks'); $this->dispatch('refreshEnvs'); - $this->dispatch('success', 'Service saved.'); + $notify && $this->dispatch('success', 'Service saved.'); } catch (\Throwable $e) { return handleError($e, $this); } finally { if (is_null($this->service->config_hash)) { - ray('asdf'); $this->service->isConfigurationChanged(true); } else { $this->dispatch('configurationChanged'); diff --git a/app/Livewire/Project/Service/Storage.php b/app/Livewire/Project/Service/Storage.php index 161c38097..4b64a8b5e 100644 --- a/app/Livewire/Project/Service/Storage.php +++ b/app/Livewire/Project/Service/Storage.php @@ -9,14 +9,36 @@ class Storage extends Component { public $resource; + public $fileStorage; + public function getListeners() { + $teamId = auth()->user()->currentTeam()->id; + return [ + "echo-private:team.{$teamId},FileStorageChanged" => 'refreshStoragesFromEvent', + 'refreshStorages', 'addNewVolume', - 'refresh_storages' => '$refresh', ]; } + public function mount() + { + $this->refreshStorages(); + } + + public function refreshStoragesFromEvent() + { + $this->refreshStorages(); + $this->dispatch('warning', 'File storage changed. Usually it means that the file / directory is already defined on the server, so Coolify set it up for you properly on the UI.'); + } + + public function refreshStorages() + { + $this->fileStorage = $this->resource->fileStorages()->get(); + $this->dispatch('$refresh'); + } + public function addNewVolume($data) { try { @@ -30,7 +52,7 @@ class Storage extends Component $this->resource->refresh(); $this->dispatch('success', 'Storage added successfully'); $this->dispatch('clearAddStorage'); - $this->dispatch('refresh_storages'); + $this->dispatch('refreshStorages'); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php index e754749a4..a0b4ac2c4 100644 --- a/app/Livewire/Project/Shared/Danger.php +++ b/app/Livewire/Project/Shared/Danger.php @@ -3,6 +3,12 @@ namespace App\Livewire\Project\Shared; use App\Jobs\DeleteResourceJob; +use App\Models\InstanceSettings; +use App\Models\Service; +use App\Models\ServiceApplication; +use App\Models\ServiceDatabase; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; use Livewire\Component; use Visus\Cuid2\Cuid2; @@ -10,28 +16,94 @@ class Danger extends Component { public $resource; + public $resourceName; + public $projectUuid; public $environmentName; public bool $delete_configurations = true; + public bool $delete_volumes = true; + + public bool $docker_cleanup = true; + + public bool $delete_connected_networks = true; + public ?string $modalId = null; + public string $resourceDomain = ''; + public function mount() { - $this->modalId = new Cuid2(7); $parameters = get_route_parameters(); + $this->modalId = new Cuid2; $this->projectUuid = data_get($parameters, 'project_uuid'); $this->environmentName = data_get($parameters, 'environment_name'); + + if ($this->resource === null) { + if (isset($parameters['service_uuid'])) { + $this->resource = Service::where('uuid', $parameters['service_uuid'])->first(); + } elseif (isset($parameters['stack_service_uuid'])) { + $this->resource = ServiceApplication::where('uuid', $parameters['stack_service_uuid'])->first() + ?? ServiceDatabase::where('uuid', $parameters['stack_service_uuid'])->first(); + } + } + + if ($this->resource === null) { + $this->resourceName = 'Unknown Resource'; + + return; + } + + if (! method_exists($this->resource, 'type')) { + $this->resourceName = 'Unknown Resource'; + + return; + } + + $this->resourceName = match ($this->resource->type()) { + 'application' => $this->resource->name ?? 'Application', + 'standalone-postgresql', + 'standalone-redis', + 'standalone-mongodb', + 'standalone-mysql', + 'standalone-mariadb', + 'standalone-keydb', + 'standalone-dragonfly', + 'standalone-clickhouse' => $this->resource->name ?? 'Database', + 'service' => $this->resource->name ?? 'Service', + 'service-application' => $this->resource->name ?? 'Service Application', + 'service-database' => $this->resource->name ?? 'Service Database', + default => 'Unknown Resource', + }; } - public function delete() + public function delete($password) { + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); + + return; + } + } + + if (! $this->resource) { + $this->addError('resource', 'Resource not found.'); + + return; + } + try { - // $this->authorize('delete', $this->resource); $this->resource->delete(); - DeleteResourceJob::dispatch($this->resource, $this->delete_configurations); + DeleteResourceJob::dispatch( + $this->resource, + $this->delete_configurations, + $this->delete_volumes, + $this->docker_cleanup, + $this->delete_connected_networks + ); return redirect()->route('project.resource.index', [ 'project_uuid' => $this->projectUuid, @@ -41,4 +113,19 @@ class Danger extends Component return handleError($e, $this); } } + + public function render() + { + return view('livewire.project.shared.danger', [ + 'checkboxes' => [ + ['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')], + ['id' => 'delete_connected_networks', 'label' => __('resource.delete_connected_networks')], + ['id' => 'delete_configurations', 'label' => __('resource.delete_configurations')], + ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')], + // ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'], + // ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'], + // ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.'] + ], + ]); + } } diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php index 22ada8ab8..c305e817c 100644 --- a/app/Livewire/Project/Shared/Destination.php +++ b/app/Livewire/Project/Shared/Destination.php @@ -5,9 +5,11 @@ namespace App\Livewire\Project\Shared; use App\Actions\Application\StopApplicationOneServer; use App\Actions\Docker\GetContainersStatus; use App\Events\ApplicationStatusChanged; -use App\Jobs\ContainerStatusJob; +use App\Models\InstanceSettings; use App\Models\Server; use App\Models\StandaloneDocker; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; use Livewire\Component; use Visus\Cuid2\Cuid2; @@ -67,7 +69,7 @@ class Destination extends Component return; } - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; $server = Server::find($server_id); $destination = StandaloneDocker::find($network_id); queue_application_deployment( @@ -115,8 +117,16 @@ class Destination extends Component ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id')); } - public function removeServer(int $network_id, int $server_id) + public function removeServer(int $network_id, int $server_id, $password) { + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); + + return; + } + } + if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) { $this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.'); diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php index b732b6b52..0dbf0f957 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php @@ -48,14 +48,6 @@ class Add extends Component public function submit() { $this->validate(); - if (str($this->value)->startsWith('{{') && str($this->value)->endsWith('}}')) { - $type = str($this->value)->after('{{')->before('.')->value; - if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { - $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); - - return; - } - } $this->dispatch('saveKey', [ 'key' => $this->key, 'value' => $this->value, diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index 4c06bfe23..787d33a69 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -23,8 +23,9 @@ class All extends Component public string $view = 'normal'; protected $listeners = [ - 'refreshEnvs', 'saveKey' => 'submit', + 'refreshEnvs', + 'environmentVariableDeleted' => 'refreshEnvs', ]; protected $rules = [ @@ -34,227 +35,234 @@ class All extends Component public function mount() { $this->resourceClass = get_class($this->resource); - $resourceWithPreviews = ['App\Models\Application']; + $resourceWithPreviews = [\App\Models\Application::class]; $simpleDockerfile = ! is_null(data_get($this->resource, 'dockerfile')); if (str($this->resourceClass)->contains($resourceWithPreviews) && ! $simpleDockerfile) { $this->showPreview = true; } - $this->modalId = new Cuid2(7); - $this->sortMe(); - $this->getDevView(); - } - - public function sortMe() - { - if ($this->resourceClass === 'App\Models\Application' && data_get($this->resource, 'build_pack') !== 'dockercompose') { - if ($this->resource->settings->is_env_sorting_enabled) { - $this->resource->environment_variables = $this->resource->environment_variables->sortBy('key'); - $this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('key'); - } else { - $this->resource->environment_variables = $this->resource->environment_variables->sortBy('id'); - $this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('id'); - } - } - $this->getDevView(); + $this->modalId = new Cuid2; + $this->sortEnvironmentVariables(); } public function instantSave() { - if ($this->resourceClass === 'App\Models\Application' && data_get($this->resource, 'build_pack') !== 'dockercompose') { - $this->resource->settings->save(); - $this->dispatch('success', 'Environment variable settings updated.'); - $this->sortMe(); + $this->resource->settings->save(); + $this->sortEnvironmentVariables(); + $this->dispatch('success', 'Environment variable settings updated.'); + } + + public function sortEnvironmentVariables() + { + if (! data_get($this->resource, 'settings.is_env_sorting_enabled')) { + if ($this->resource->environment_variables) { + $this->resource->environment_variables = $this->resource->environment_variables->sortBy('order')->values(); + } + + if ($this->resource->environment_variables_preview) { + $this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('order')->values(); + } } + + $this->getDevView(); } public function getDevView() { - $this->variables = $this->resource->environment_variables->map(function ($item) { + $this->variables = $this->formatEnvironmentVariables($this->resource->environment_variables); + if ($this->showPreview) { + $this->variablesPreview = $this->formatEnvironmentVariables($this->resource->environment_variables_preview); + } + } + + private function formatEnvironmentVariables($variables) + { + return $variables->map(function ($item) { if ($item->is_shown_once) { - return "$item->key=(locked secret)"; + return "$item->key=(Locked Secret, delete and add again to change)"; } if ($item->is_multiline) { - return "$item->key=(multiline, edit in normal view)"; + return "$item->key=(Multiline environment variable, edit in normal view)"; } return "$item->key=$item->value"; - })->join(' -'); - if ($this->showPreview) { - $this->variablesPreview = $this->resource->environment_variables_preview->map(function ($item) { - if ($item->is_shown_once) { - return "$item->key=(locked secret)"; - } - if ($item->is_multiline) { - return "$item->key=(multiline, edit in normal view)"; - } - - return "$item->key=$item->value"; - })->join(' -'); - } + })->join("\n"); } public function switch() { - if ($this->view === 'normal') { - $this->view = 'dev'; - } else { - $this->view = 'normal'; - } - $this->sortMe(); + $this->view = $this->view === 'normal' ? 'dev' : 'normal'; + $this->sortEnvironmentVariables(); } - public function saveVariables($isPreview) + public function submit($data = null) { - if ($isPreview) { - $variables = parseEnvFormatToArray($this->variablesPreview); - $this->resource->environment_variables_preview()->whereNotIn('key', array_keys($variables))->delete(); - } else { - $variables = parseEnvFormatToArray($this->variables); - ray($variables, $this->variables); - $this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->delete(); - } - foreach ($variables as $key => $variable) { - if ($isPreview) { - $found = $this->resource->environment_variables_preview()->where('key', $key)->first(); + try { + if ($data === null) { + $this->handleBulkSubmit(); } else { - $found = $this->resource->environment_variables()->where('key', $key)->first(); + $this->handleSingleSubmit($data); } + + $this->updateOrder(); + $this->sortEnvironmentVariables(); + } catch (\Throwable $e) { + return handleError($e, $this); + } finally { + $this->refreshEnvs(); + } + } + + private function updateOrder() + { + $variables = parseEnvFormatToArray($this->variables); + $order = 1; + foreach ($variables as $key => $value) { + $env = $this->resource->environment_variables()->where('key', $key)->first(); + if ($env) { + $env->order = $order; + $env->save(); + } + $order++; + } + + if ($this->showPreview) { + $previewVariables = parseEnvFormatToArray($this->variablesPreview); + $order = 1; + foreach ($previewVariables as $key => $value) { + $env = $this->resource->environment_variables_preview()->where('key', $key)->first(); + if ($env) { + $env->order = $order; + $env->save(); + } + $order++; + } + } + } + + private function handleBulkSubmit() + { + $variables = parseEnvFormatToArray($this->variables); + $this->deleteRemovedVariables(false, $variables); + $this->updateOrCreateVariables(false, $variables); + + if ($this->showPreview) { + $previewVariables = parseEnvFormatToArray($this->variablesPreview); + $this->deleteRemovedVariables(true, $previewVariables); + $this->updateOrCreateVariables(true, $previewVariables); + } + + $this->dispatch('success', 'Environment variables updated.'); + } + + private function handleSingleSubmit($data) + { + $found = $this->resource->environment_variables()->where('key', $data['key'])->first(); + if ($found) { + $this->dispatch('error', 'Environment variable already exists.'); + + return; + } + + $maxOrder = $this->resource->environment_variables()->max('order') ?? 0; + $environment = $this->createEnvironmentVariable($data); + $environment->order = $maxOrder + 1; + $environment->save(); + } + + private function createEnvironmentVariable($data) + { + $environment = new EnvironmentVariable; + $environment->key = $data['key']; + $environment->value = $data['value']; + $environment->is_build_time = $data['is_build_time'] ?? false; + $environment->is_multiline = $data['is_multiline'] ?? false; + $environment->is_literal = $data['is_literal'] ?? false; + $environment->is_preview = $data['is_preview'] ?? false; + + $resourceType = $this->resource->type(); + $resourceIdField = $this->getResourceIdField($resourceType); + + if ($resourceIdField) { + $environment->$resourceIdField = $this->resource->id; + } + + return $environment; + } + + private function getResourceIdField($resourceType) + { + $resourceTypes = [ + 'application' => 'application_id', + 'standalone-postgresql' => 'standalone_postgresql_id', + 'standalone-redis' => 'standalone_redis_id', + 'standalone-mongodb' => 'standalone_mongodb_id', + 'standalone-mysql' => 'standalone_mysql_id', + 'standalone-mariadb' => 'standalone_mariadb_id', + 'standalone-keydb' => 'standalone_keydb_id', + 'standalone-dragonfly' => 'standalone_dragonfly_id', + 'standalone-clickhouse' => 'standalone_clickhouse_id', + 'service' => 'service_id', + ]; + + return $resourceTypes[$resourceType] ?? null; + } + + private function deleteRemovedVariables($isPreview, $variables) + { + $method = $isPreview ? 'environment_variables_preview' : 'environment_variables'; + $this->resource->$method()->whereNotIn('key', array_keys($variables))->delete(); + } + + private function updateOrCreateVariables($isPreview, $variables) + { + foreach ($variables as $key => $value) { + $method = $isPreview ? 'environment_variables_preview' : 'environment_variables'; + $found = $this->resource->$method()->where('key', $key)->first(); + if ($found) { - if ($found->is_shown_once || $found->is_multiline) { - continue; + if (! $found->is_shown_once && ! $found->is_multiline) { + $found->value = $value; + $found->save(); } - $found->value = $variable; - if (str($found->value)->startsWith('{{') && str($found->value)->endsWith('}}')) { - $type = str($found->value)->after('{{')->before('.')->value; - if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { - $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); - - return; - } - } - $found->save(); - - continue; } else { - $environment = new EnvironmentVariable(); + $environment = new EnvironmentVariable; $environment->key = $key; - $environment->value = $variable; - if (str($environment->value)->startsWith('{{') && str($environment->value)->endsWith('}}')) { - $type = str($environment->value)->after('{{')->before('.')->value; - if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { - $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); - - return; - } - } + $environment->value = $value; $environment->is_build_time = false; $environment->is_multiline = false; - $environment->is_preview = $isPreview ? true : false; - switch ($this->resource->type()) { - case 'application': - $environment->application_id = $this->resource->id; - break; - case 'standalone-postgresql': - $environment->standalone_postgresql_id = $this->resource->id; - break; - case 'standalone-redis': - $environment->standalone_redis_id = $this->resource->id; - break; - case 'standalone-mongodb': - $environment->standalone_mongodb_id = $this->resource->id; - break; - case 'standalone-mysql': - $environment->standalone_mysql_id = $this->resource->id; - break; - case 'standalone-mariadb': - $environment->standalone_mariadb_id = $this->resource->id; - break; - case 'standalone-keydb': - $environment->standalone_keydb_id = $this->resource->id; - break; - case 'standalone-dragonfly': - $environment->standalone_dragonfly_id = $this->resource->id; - break; - case 'standalone-clickhouse': - $environment->standalone_clickhouse_id = $this->resource->id; - break; - case 'service': - $environment->service_id = $this->resource->id; - break; - } + $environment->is_preview = $isPreview; + + $this->setEnvironmentResourceId($environment); $environment->save(); } } - if ($isPreview) { - $this->dispatch('success', 'Preview environment variables updated.'); - } else { - $this->dispatch('success', 'Environment variables updated.'); + } + + private function setEnvironmentResourceId($environment) + { + $resourceTypes = [ + 'application' => 'application_id', + 'standalone-postgresql' => 'standalone_postgresql_id', + 'standalone-redis' => 'standalone_redis_id', + 'standalone-mongodb' => 'standalone_mongodb_id', + 'standalone-mysql' => 'standalone_mysql_id', + 'standalone-mariadb' => 'standalone_mariadb_id', + 'standalone-keydb' => 'standalone_keydb_id', + 'standalone-dragonfly' => 'standalone_dragonfly_id', + 'standalone-clickhouse' => 'standalone_clickhouse_id', + 'service' => 'service_id', + ]; + + $resourceType = $this->resource->type(); + if (isset($resourceTypes[$resourceType])) { + $environment->{$resourceTypes[$resourceType]} = $this->resource->id; } - $this->refreshEnvs(); } public function refreshEnvs() { $this->resource->refresh(); + $this->sortEnvironmentVariables(); $this->getDevView(); } - - public function submit($data) - { - try { - $found = $this->resource->environment_variables()->where('key', $data['key'])->first(); - if ($found) { - $this->dispatch('error', 'Environment variable already exists.'); - - return; - } - $environment = new EnvironmentVariable(); - $environment->key = $data['key']; - $environment->value = $data['value']; - $environment->is_build_time = $data['is_build_time']; - $environment->is_multiline = $data['is_multiline']; - $environment->is_literal = $data['is_literal']; - $environment->is_preview = $data['is_preview']; - - switch ($this->resource->type()) { - case 'application': - $environment->application_id = $this->resource->id; - break; - case 'standalone-postgresql': - $environment->standalone_postgresql_id = $this->resource->id; - break; - case 'standalone-redis': - $environment->standalone_redis_id = $this->resource->id; - break; - case 'standalone-mongodb': - $environment->standalone_mongodb_id = $this->resource->id; - break; - case 'standalone-mysql': - $environment->standalone_mysql_id = $this->resource->id; - break; - case 'standalone-mariadb': - $environment->standalone_mariadb_id = $this->resource->id; - break; - case 'standalone-keydb': - $environment->standalone_keydb_id = $this->resource->id; - break; - case 'standalone-dragonfly': - $environment->standalone_dragonfly_id = $this->resource->id; - break; - case 'standalone-clickhouse': - $environment->standalone_clickhouse_id = $this->resource->id; - break; - case 'service': - $environment->service_id = $this->resource->id; - break; - } - $environment->save(); - $this->refreshEnvs(); - $this->dispatch('success', 'Environment variable added.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } } diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index e77c05d6b..e71cd9f42 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -24,6 +24,8 @@ class Show extends Component public string $type; protected $listeners = [ + 'refreshEnvs' => 'refresh', + 'refresh', 'compose_loaded' => '$refresh', ]; @@ -35,6 +37,7 @@ class Show extends Component 'env.is_literal' => 'required|boolean', 'env.is_shown_once' => 'required|boolean', 'env.real_value' => 'nullable', + 'env.is_required' => 'required|boolean', ]; protected $validationAttributes = [ @@ -44,14 +47,21 @@ class Show extends Component 'env.is_multiline' => 'Multiline', 'env.is_literal' => 'Literal', 'env.is_shown_once' => 'Shown Once', + 'env.is_required' => 'Required', ]; + public function refresh() + { + $this->env->refresh(); + $this->checkEnvs(); + } + public function mount() { - if ($this->env->getMorphClass() === 'App\Models\SharedEnvironmentVariable') { + if ($this->env->getMorphClass() === \App\Models\SharedEnvironmentVariable::class) { $this->isSharedVariable = true; } - $this->modalId = new Cuid2(7); + $this->modalId = new Cuid2; $this->parameters = get_route_parameters(); $this->checkEnvs(); } @@ -70,7 +80,7 @@ class Show extends Component public function serialize() { data_forget($this->env, 'real_value'); - if ($this->env->getMorphClass() === 'App\Models\SharedEnvironmentVariable') { + if ($this->env->getMorphClass() === \App\Models\SharedEnvironmentVariable::class) { data_forget($this->env, 'is_build_time'); } } @@ -101,18 +111,24 @@ class Show extends Component } else { $this->validate(); } - if (str($this->env->value)->startsWith('{{') && str($this->env->value)->endsWith('}}')) { - $type = str($this->env->value)->after('{{')->before('.')->value; - if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { - $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); - return; - } + if (! $this->isSharedVariable && $this->env->is_required && str($this->env->real_value)->isEmpty()) { + $oldValue = $this->env->getOriginal('value'); + $this->env->value = $oldValue; + $this->dispatch('error', 'Required environment variable cannot be empty.'); + + return; } + $this->serialize(); + + if ($this->isSharedVariable) { + unset($this->env->is_required); + } + $this->env->save(); $this->dispatch('success', 'Environment variable updated.'); - $this->dispatch('refreshEnvs'); + $this->dispatch('envsUpdated'); } catch (\Exception $e) { return handleError($e); } @@ -122,7 +138,8 @@ class Show extends Component { try { $this->env->delete(); - $this->dispatch('refreshEnvs'); + $this->dispatch('environmentVariableDeleted'); + $this->dispatch('success', 'Environment variable deleted successfully.'); } catch (\Exception $e) { return handleError($e); } diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index dc3a62c56..621ab1bac 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -6,13 +6,14 @@ use App\Models\Application; use App\Models\Server; use App\Models\Service; use Illuminate\Support\Collection; +use Livewire\Attributes\On; use Livewire\Component; class ExecuteContainerCommand extends Component { - public string $command; + public $selected_container = 'default'; - public string $container; + public $container; public Collection $containers; @@ -22,8 +23,6 @@ class ExecuteContainerCommand extends Component public string $type; - public string $workDir = ''; - public Server $server; public Collection $servers; @@ -32,11 +31,13 @@ class ExecuteContainerCommand extends Component 'server' => 'required', 'container' => 'required', 'command' => 'required', - 'workDir' => 'nullable', ]; public function mount() { + if (! auth()->user()->isAdmin()) { + abort(403); + } $this->parameters = get_route_parameters(); $this->containers = collect(); $this->servers = collect(); @@ -51,6 +52,7 @@ class ExecuteContainerCommand extends Component $this->servers = $this->servers->push($server); } } + $this->loadContainers(); } elseif (data_get($this->parameters, 'database_uuid')) { $this->type = 'database'; $resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id')); @@ -61,23 +63,18 @@ class ExecuteContainerCommand extends Component if ($this->resource->destination->server->isFunctional()) { $this->servers = $this->servers->push($this->resource->destination->server); } - $this->container = $this->resource->uuid; - $this->containers->push($this->container); + $this->loadContainers(); } elseif (data_get($this->parameters, 'service_uuid')) { $this->type = 'service'; $this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail(); - $this->resource->applications()->get()->each(function ($application) { - $this->containers->push(data_get($application, 'name').'-'.data_get($this->resource, 'uuid')); - }); - $this->resource->databases()->get()->each(function ($database) { - $this->containers->push(data_get($database, 'name').'-'.data_get($this->resource, 'uuid')); - }); if ($this->resource->server->isFunctional()) { $this->servers = $this->servers->push($this->resource->server); } - } - if ($this->containers->count() > 0) { - $this->container = $this->containers->first(); + $this->loadContainers(); + } elseif (data_get($this->parameters, 'server_uuid')) { + $this->type = 'server'; + $this->resource = Server::where('uuid', $this->parameters['server_uuid'])->firstOrFail(); + $this->server = $this->resource; } } @@ -95,50 +92,97 @@ class ExecuteContainerCommand extends Component $containers = getCurrentApplicationContainerStatus($server, $this->resource->id, includePullrequests: true); } foreach ($containers as $container) { - $payload = [ - 'server' => $server, - 'container' => $container, - ]; - $this->containers = $this->containers->push($payload); + // if container state is running + if (data_get($container, 'State') === 'running') { + $payload = [ + 'server' => $server, + 'container' => $container, + ]; + $this->containers = $this->containers->push($payload); + } } + } elseif (data_get($this->parameters, 'database_uuid')) { + if ($this->resource->isRunning()) { + $this->containers = $this->containers->push([ + 'server' => $server, + 'container' => [ + 'Names' => $this->resource->uuid, + ], + ]); + } + } elseif (data_get($this->parameters, 'service_uuid')) { + $this->resource->applications()->get()->each(function ($application) { + if ($application->isRunning()) { + $this->containers->push([ + 'server' => $this->resource->server, + 'container' => [ + 'Names' => data_get($application, 'name').'-'.data_get($this->resource, 'uuid'), + ], + ]); + } + }); + $this->resource->databases()->get()->each(function ($database) { + if ($database->isRunning()) { + $this->containers->push([ + 'server' => $this->resource->server, + 'container' => [ + 'Names' => data_get($database, 'name').'-'.data_get($this->resource, 'uuid'), + ], + ]); + } + }); } } if ($this->containers->count() > 0) { - if (data_get($this->parameters, 'application_uuid')) { - $this->container = data_get($this->containers->first(), 'container.Names'); - } elseif (data_get($this->parameters, 'database_uuid')) { - $this->container = $this->containers->first(); - } elseif (data_get($this->parameters, 'service_uuid')) { - $this->container = $this->containers->first(); - } + $this->container = $this->containers->first(); + } + if ($this->containers->count() === 1) { + $this->selected_container = data_get($this->containers->first(), 'container.Names'); } } - public function runCommand() + #[On('connectToServer')] + public function connectToServer() { try { - if (data_get($this->parameters, 'application_uuid')) { - $container = $this->containers->where('container.Names', $this->container)->first(); - $container_name = data_get($container, 'container.Names'); - if (is_null($container)) { - throw new \RuntimeException('Container not found.'); - } - $server = data_get($container, 'server'); - } else { - $container_name = $this->container; - $server = $this->servers->first(); + if ($this->server->isForceDisabled()) { + throw new \RuntimeException('Server is disabled.'); } + $this->dispatch( + 'send-terminal-command', + false, + data_get($this->server, 'name'), + data_get($this->server, 'uuid') + ); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + #[On('connectToContainer')] + public function connectToContainer() + { + if ($this->selected_container === 'default') { + $this->dispatch('error', 'Please select a container.'); + + return; + } + try { + $container = collect($this->containers)->firstWhere('container.Names', $this->selected_container); + if (is_null($container)) { + throw new \RuntimeException('Container not found.'); + } + $server = data_get($this->container, 'server'); + if ($server->isForceDisabled()) { throw new \RuntimeException('Server is disabled.'); } - $cmd = "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; ".str_replace("'", "'\''", $this->command)."'"; - if (! empty($this->workDir)) { - $exec = "docker exec -w {$this->workDir} {$container_name} {$cmd}"; - } else { - $exec = "docker exec {$container_name} {$cmd}"; - } - $activity = remote_process([$exec], $server, ignore_errors: true); - $this->dispatch('activityMonitor', $activity->id); + $this->dispatch( + 'send-terminal-command', + isset($container), + data_get($container, 'container.Names'), + data_get($container, 'server.uuid') + ); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Shared/GetLogs.php b/app/Livewire/Project/Shared/GetLogs.php index edcaf0f34..43fd97c34 100644 --- a/app/Livewire/Project/Shared/GetLogs.php +++ b/app/Livewire/Project/Shared/GetLogs.php @@ -2,6 +2,7 @@ namespace App\Livewire\Project\Shared; +use App\Helpers\SshMultiplexingHelper; use App\Models\Application; use App\Models\Server; use App\Models\Service; @@ -38,12 +39,12 @@ class GetLogs extends Component public ?bool $showTimeStamps = true; - public int $numberOfLines = 100; + public ?int $numberOfLines = 100; public function mount() { if (! is_null($this->resource)) { - if ($this->resource->getMorphClass() === 'App\Models\Application') { + if ($this->resource->getMorphClass() === \App\Models\Application::class) { $this->showTimeStamps = $this->resource->settings->is_include_timestamps; } else { if ($this->servicesubtype) { @@ -52,7 +53,7 @@ class GetLogs extends Component $this->showTimeStamps = $this->resource->is_include_timestamps; } } - if ($this->resource?->getMorphClass() === 'App\Models\Application') { + if ($this->resource?->getMorphClass() === \App\Models\Application::class) { if (str($this->container)->contains('-pr-')) { $this->pull_request = 'Pull Request: '.str($this->container)->afterLast('-pr-')->beforeLast('_')->value(); } @@ -68,11 +69,11 @@ class GetLogs extends Component public function instantSave() { if (! is_null($this->resource)) { - if ($this->resource->getMorphClass() === 'App\Models\Application') { + if ($this->resource->getMorphClass() === \App\Models\Application::class) { $this->resource->settings->is_include_timestamps = $this->showTimeStamps; $this->resource->settings->save(); } - if ($this->resource->getMorphClass() === 'App\Models\Service') { + if ($this->resource->getMorphClass() === \App\Models\Service::class) { $serviceName = str($this->container)->beforeLast('-')->value(); $subType = $this->resource->applications()->where('name', $serviceName)->first(); if ($subType) { @@ -94,10 +95,10 @@ class GetLogs extends Component if (! $this->server->isFunctional()) { return; } - if (! $refresh && ($this->resource?->getMorphClass() === 'App\Models\Service' || str($this->container)->contains('-pr-'))) { + if (! $refresh && ($this->resource?->getMorphClass() === \App\Models\Service::class || str($this->container)->contains('-pr-'))) { return; } - if (! $this->numberOfLines) { + if ($this->numberOfLines <= 0 || is_null($this->numberOfLines)) { $this->numberOfLines = 1000; } if ($this->container) { @@ -108,14 +109,14 @@ class GetLogs extends Component $command = parseCommandsByLineForSudo(collect($command), $this->server); $command = $command[0]; } - $sshCommand = generateSshCommand($this->server, $command); + $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command); } else { $command = "docker logs -n {$this->numberOfLines} -t {$this->container}"; if ($this->server->isNonRoot()) { $command = parseCommandsByLineForSudo(collect($command), $this->server); $command = $command[0]; } - $sshCommand = generateSshCommand($this->server, $command); + $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command); } } else { if ($this->server->isSwarm()) { @@ -124,14 +125,14 @@ class GetLogs extends Component $command = parseCommandsByLineForSudo(collect($command), $this->server); $command = $command[0]; } - $sshCommand = generateSshCommand($this->server, $command); + $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command); } else { $command = "docker logs -n {$this->numberOfLines} {$this->container}"; if ($this->server->isNonRoot()) { $command = parseCommandsByLineForSudo(collect($command), $this->server); $command = $command[0]; } - $sshCommand = generateSshCommand($this->server, $command); + $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command); } } if ($refresh) { diff --git a/app/Livewire/Project/Shared/Logs.php b/app/Livewire/Project/Shared/Logs.php index 008d743ed..12022b1ee 100644 --- a/app/Livewire/Project/Shared/Logs.php +++ b/app/Livewire/Project/Shared/Logs.php @@ -59,15 +59,6 @@ class Logs extends Component } } - public function loadMetrics() - { - return; - $server = data_get($this->resource, 'destination.server'); - if ($server->isFunctional()) { - $this->cpu = $server->getMetrics(); - } - } - public function mount() { try { @@ -118,11 +109,7 @@ class Logs extends Component $this->containers = $this->containers->filter(function ($container) { return str_contains($container, $this->query['pull_request_id']); }); - ray($this->containers); - } - - $this->loadMetrics(); } catch (\Exception $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Shared/Metrics.php b/app/Livewire/Project/Shared/Metrics.php new file mode 100644 index 000000000..fdc35fc0f --- /dev/null +++ b/app/Livewire/Project/Shared/Metrics.php @@ -0,0 +1,59 @@ +poll || $this->interval <= 10) { + $this->loadData(); + if ($this->interval > 10) { + $this->poll = false; + } + } + } + + public function loadData() + { + try { + $cpuMetrics = $this->resource->getCpuMetrics($this->interval); + $memoryMetrics = $this->resource->getMemoryMetrics($this->interval); + $this->dispatch("refreshChartData-{$this->chartId}-cpu", [ + 'seriesData' => $cpuMetrics, + ]); + $this->dispatch("refreshChartData-{$this->chartId}-memory", [ + 'seriesData' => $memoryMetrics, + ]); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function setInterval() + { + if ($this->interval <= 10) { + $this->poll = true; + } + $this->loadData(); + } + + public function render() + { + return view('livewire.project.shared.metrics'); + } +} diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index 586a125ae..e67df6aa9 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -39,9 +39,9 @@ class ResourceOperations extends Component if (! $new_destination) { return $this->addError('destination_id', 'Destination not found.'); } - $uuid = (string) new Cuid2(7); + $uuid = (string) new Cuid2; $server = $new_destination->server; - if ($this->resource->getMorphClass() === 'App\Models\Application') { + if ($this->resource->getMorphClass() === \App\Models\Application::class) { $new_resource = $this->resource->replicate()->fill([ 'uuid' => $uuid, 'name' => $this->resource->name.'-clone-'.$uuid, @@ -78,16 +78,16 @@ class ResourceOperations extends Component return redirect()->to($route); } elseif ( - $this->resource->getMorphClass() === 'App\Models\StandalonePostgresql' || - $this->resource->getMorphClass() === 'App\Models\StandaloneMongodb' || - $this->resource->getMorphClass() === 'App\Models\StandaloneMysql' || - $this->resource->getMorphClass() === 'App\Models\StandaloneMariadb' || - $this->resource->getMorphClass() === 'App\Models\StandaloneRedis' || - $this->resource->getMorphClass() === 'App\Models\StandaloneKeydb' || - $this->resource->getMorphClass() === 'App\Models\StandaloneDragonfly' || - $this->resource->getMorphClass() === 'App\Models\StandaloneClickhouse' + $this->resource->getMorphClass() === \App\Models\StandalonePostgresql::class || + $this->resource->getMorphClass() === \App\Models\StandaloneMongodb::class || + $this->resource->getMorphClass() === \App\Models\StandaloneMysql::class || + $this->resource->getMorphClass() === \App\Models\StandaloneMariadb::class || + $this->resource->getMorphClass() === \App\Models\StandaloneRedis::class || + $this->resource->getMorphClass() === \App\Models\StandaloneKeydb::class || + $this->resource->getMorphClass() === \App\Models\StandaloneDragonfly::class || + $this->resource->getMorphClass() === \App\Models\StandaloneClickhouse::class ) { - $uuid = (string) new Cuid2(7); + $uuid = (string) new Cuid2; $new_resource = $this->resource->replicate()->fill([ 'uuid' => $uuid, 'name' => $this->resource->name.'-clone-'.$uuid, @@ -121,7 +121,7 @@ class ResourceOperations extends Component return redirect()->to($route); } elseif ($this->resource->type() === 'service') { - $uuid = (string) new Cuid2(7); + $uuid = (string) new Cuid2; $new_resource = $this->resource->replicate()->fill([ 'uuid' => $uuid, 'name' => $this->resource->name.'-clone-'.$uuid, @@ -147,7 +147,6 @@ class ResourceOperations extends Component return redirect()->to($route); } - } public function moveTo($environment_id) diff --git a/app/Livewire/Project/Shared/ScheduledTask/Add.php b/app/Livewire/Project/Shared/ScheduledTask/Add.php index f36b7b141..adfd59217 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Add.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Add.php @@ -55,8 +55,8 @@ class Add extends Component return; } - if (empty($this->container) || $this->container == 'null') { - if ($this->type == 'service') { + if (empty($this->container) || $this->container === 'null') { + if ($this->type === 'service') { $this->container = $this->subServiceName; } } diff --git a/app/Livewire/Project/Shared/ScheduledTask/All.php b/app/Livewire/Project/Shared/ScheduledTask/All.php index 1aa5a2b87..6ab8426f3 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/All.php +++ b/app/Livewire/Project/Shared/ScheduledTask/All.php @@ -21,12 +21,12 @@ class All extends Component public function mount() { $this->parameters = get_route_parameters(); - if ($this->resource->type() == 'service') { + if ($this->resource->type() === 'service') { $this->containerNames = $this->resource->applications()->pluck('name'); $this->containerNames = $this->containerNames->merge($this->resource->databases()->pluck('name')); - } elseif ($this->resource->type() == 'application') { + } elseif ($this->resource->type() === 'application') { if ($this->resource->build_pack === 'dockercompose') { - $parsed = $this->resource->parseCompose(); + $parsed = $this->resource->parse(); $containers = collect(data_get($parsed, 'services'))->keys(); $this->containerNames = $containers; } else { @@ -43,7 +43,7 @@ class All extends Component public function submit($data) { try { - $task = new ScheduledTask(); + $task = new ScheduledTask; $task->name = $data['name']; $task->command = $data['command']; $task->frequency = $data['frequency']; diff --git a/app/Livewire/Project/Shared/ScheduledTask/Executions.php b/app/Livewire/Project/Shared/ScheduledTask/Executions.php index 7a2e14e89..0710e37ff 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Executions.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Executions.php @@ -2,21 +2,60 @@ namespace App\Livewire\Project\Shared\ScheduledTask; +use App\Models\ScheduledTask; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Auth; +use Livewire\Attributes\Locked; use Livewire\Component; class Executions extends Component { - public $executions = []; + public ScheduledTask $task; - public $selectedKey; + #[Locked] + public int $taskId; + + #[Locked] + public Collection $executions; + + #[Locked] + public ?int $selectedKey = null; + + #[Locked] + public ?string $serverTimezone = null; public function getListeners() { + $teamId = Auth::user()->currentTeam()->id; + return [ - 'selectTask', + "echo-private:team.{$teamId},ScheduledTaskDone" => 'refreshExecutions', ]; } + public function mount($taskId) + { + try { + $this->taskId = $taskId; + $this->task = ScheduledTask::findOrFail($taskId); + $this->executions = $this->task->executions()->take(20)->get(); + $this->serverTimezone = data_get($this->task, 'application.destination.server.settings.server_timezone'); + if (! $this->serverTimezone) { + $this->serverTimezone = data_get($this->task, 'service.destination.server.settings.server_timezone'); + } + if (! $this->serverTimezone) { + $this->serverTimezone = 'UTC'; + } + } catch (\Exception $e) { + return handleError($e); + } + } + + public function refreshExecutions(): void + { + $this->executions = $this->task->executions()->take(20)->get(); + } + public function selectTask($key): void { if ($key == $this->selectedKey) { @@ -26,4 +65,17 @@ class Executions extends Component } $this->selectedKey = $key; } + + public function formatDateInServerTimezone($date) + { + $serverTimezone = $this->serverTimezone; + $dateObj = new \DateTime($date); + try { + $dateObj->setTimezone(new \DateTimeZone($serverTimezone)); + } catch (\Exception) { + $dateObj->setTimezone(new \DateTimeZone('UTC')); + } + + return $dateObj->format('Y-m-d H:i:s T'); + } } diff --git a/app/Livewire/Project/Shared/ScheduledTask/Show.php b/app/Livewire/Project/Shared/ScheduledTask/Show.php index dbd420d94..0900a1d70 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Show.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Show.php @@ -2,71 +2,124 @@ namespace App\Livewire\Project\Shared\ScheduledTask; +use App\Jobs\ScheduledTaskJob; use App\Models\Application; -use App\Models\ScheduledTask as ModelsScheduledTask; +use App\Models\ScheduledTask; use App\Models\Service; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; -use Visus\Cuid2\Cuid2; class Show extends Component { - public $parameters; - public Application|Service $resource; - public ModelsScheduledTask $task; + public ScheduledTask $task; - public ?string $modalId = null; + #[Locked] + public array $parameters; + #[Locked] public string $type; - protected $rules = [ - 'task.enabled' => 'required|boolean', - 'task.name' => 'required|string', - 'task.command' => 'required|string', - 'task.frequency' => 'required|string', - 'task.container' => 'nullable|string', - ]; + #[Validate(['boolean'])] + public bool $isEnabled = false; - protected $validationAttributes = [ - 'name' => 'name', - 'command' => 'command', - 'frequency' => 'frequency', - 'container' => 'container', - ]; + #[Validate(['string', 'required'])] + public string $name; - public function mount() + #[Validate(['string', 'required'])] + public string $command; + + #[Validate(['string', 'required'])] + public string $frequency; + + #[Validate(['string', 'nullable'])] + public ?string $container = null; + + #[Locked] + public ?string $application_uuid; + + #[Locked] + public ?string $service_uuid; + + #[Locked] + public string $task_uuid; + + public function mount(string $task_uuid, string $project_uuid, string $environment_name, ?string $application_uuid = null, ?string $service_uuid = null) { - $this->parameters = get_route_parameters(); + try { + $this->task_uuid = $task_uuid; + if ($application_uuid) { + $this->type = 'application'; + $this->application_uuid = $application_uuid; + $this->resource = Application::ownedByCurrentTeam()->where('uuid', $application_uuid)->firstOrFail(); + } elseif ($service_uuid) { + $this->type = 'service'; + $this->service_uuid = $service_uuid; + $this->resource = Service::ownedByCurrentTeam()->where('uuid', $service_uuid)->firstOrFail(); + } + $this->parameters = [ + 'environment_name' => $environment_name, + 'project_uuid' => $project_uuid, + 'application_uuid' => $application_uuid, + 'service_uuid' => $service_uuid, + ]; - if (data_get($this->parameters, 'application_uuid')) { - $this->type = 'application'; - $this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail(); - } elseif (data_get($this->parameters, 'service_uuid')) { - $this->type = 'service'; - $this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail(); + $this->task = $this->resource->scheduled_tasks()->where('uuid', $task_uuid)->firstOrFail(); + $this->syncData(); + } catch (\Exception $e) { + return handleError($e); } + } - $this->modalId = new Cuid2(7); - $this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first(); + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->task->enabled = $this->isEnabled; + $this->task->name = str($this->name)->trim()->value(); + $this->task->command = str($this->command)->trim()->value(); + $this->task->frequency = str($this->frequency)->trim()->value(); + $this->task->container = str($this->container)->trim()->value(); + $this->task->save(); + } else { + $this->isEnabled = $this->task->enabled; + $this->name = $this->task->name; + $this->command = $this->task->command; + $this->frequency = $this->task->frequency; + $this->container = $this->task->container; + } } public function instantSave() { - $this->validateOnly('task.enabled'); - $this->task->save(['enabled' => $this->task->enabled]); - $this->dispatch('success', 'Scheduled task updated.'); - $this->dispatch('refreshTasks'); + try { + $this->syncData(true); + $this->dispatch('success', 'Scheduled task updated.'); + $this->refreshTasks(); + } catch (\Exception $e) { + return handleError($e); + } } public function submit() { - $this->validate(); - $this->task->name = str($this->task->name)->trim()->value(); - $this->task->container = str($this->task->container)->trim()->value(); - $this->task->save(); - $this->dispatch('success', 'Scheduled task updated.'); - $this->dispatch('refreshTasks'); + try { + $this->syncData(true); + $this->dispatch('success', 'Scheduled task updated.'); + } catch (\Exception $e) { + return handleError($e); + } + } + + public function refreshTasks() + { + try { + $this->task->refresh(); + } catch (\Exception $e) { + return handleError($e); + } } public function delete() @@ -74,13 +127,23 @@ class Show extends Component try { $this->task->delete(); - if ($this->type == 'application') { - return redirect()->route('project.application.configuration', $this->parameters); + if ($this->type === 'application') { + return redirect()->route('project.application.configuration', $this->parameters, $this->task->name); } else { - return redirect()->route('project.service.configuration', $this->parameters); + return redirect()->route('project.service.configuration', $this->parameters, $this->task->name); } } catch (\Exception $e) { return handleError($e); } } + + public function executeNow() + { + try { + ScheduledTaskJob::dispatch($this->task); + $this->dispatch('success', 'Scheduled task executed.'); + } catch (\Exception $e) { + return handleError($e); + } + } } diff --git a/app/Livewire/Project/Shared/Storages/Add.php b/app/Livewire/Project/Shared/Storages/Add.php index d22f3b05f..6e250bd90 100644 --- a/app/Livewire/Project/Shared/Storages/Add.php +++ b/app/Livewire/Project/Shared/Storages/Add.php @@ -54,7 +54,11 @@ class Add extends Component public function mount() { - $this->file_storage_directory_source = application_configuration_dir()."/{$this->resource->uuid}"; + if (str($this->resource->getMorphClass())->contains('Standalone')) { + $this->file_storage_directory_source = database_configuration_dir()."/{$this->resource->uuid}"; + } else { + $this->file_storage_directory_source = application_configuration_dir()."/{$this->resource->uuid}"; + } $this->uuid = $this->resource->uuid; $this->parameters = get_route_parameters(); if (data_get($this->parameters, 'application_uuid')) { @@ -79,7 +83,7 @@ class Add extends Component ]); $this->file_storage_path = trim($this->file_storage_path); $this->file_storage_path = str($this->file_storage_path)->start('/')->value(); - if ($this->resource->getMorphClass() === 'App\Models\Application') { + if ($this->resource->getMorphClass() === \App\Models\Application::class) { $fs_path = application_configuration_dir().'/'.$this->resource->uuid.$this->file_storage_path; } LocalFileVolume::create( @@ -92,11 +96,10 @@ class Add extends Component 'resource_type' => get_class($this->resource), ], ); - $this->dispatch('refresh_storages'); + $this->dispatch('refreshStorages'); } catch (\Throwable $e) { return handleError($e, $this); } - } public function submitFileStorageDirectory() @@ -119,11 +122,10 @@ class Add extends Component 'resource_type' => get_class($this->resource), ], ); - $this->dispatch('refresh_storages'); + $this->dispatch('refreshStorages'); } catch (\Throwable $e) { return handleError($e, $this); } - } public function submitPersistentVolume() @@ -140,7 +142,6 @@ class Add extends Component 'mount_path' => $this->mount_path, 'host_path' => $this->host_path, ]); - } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Shared/Storages/All.php b/app/Livewire/Project/Shared/Storages/All.php index d2014694e..c26315d3b 100644 --- a/app/Livewire/Project/Shared/Storages/All.php +++ b/app/Livewire/Project/Shared/Storages/All.php @@ -8,5 +8,5 @@ class All extends Component { public $resource; - protected $listeners = ['refresh_storages' => '$refresh']; + protected $listeners = ['refreshStorages' => '$refresh']; } diff --git a/app/Livewire/Project/Shared/Storages/Show.php b/app/Livewire/Project/Shared/Storages/Show.php index 52b52ef6d..54b1be3af 100644 --- a/app/Livewire/Project/Shared/Storages/Show.php +++ b/app/Livewire/Project/Shared/Storages/Show.php @@ -2,9 +2,11 @@ namespace App\Livewire\Project\Shared\Storages; +use App\Models\InstanceSettings; use App\Models\LocalPersistentVolume; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; use Livewire\Component; -use Visus\Cuid2\Cuid2; class Show extends Component { @@ -12,8 +14,6 @@ class Show extends Component public bool $isReadOnly = false; - public ?string $modalId = null; - public bool $isFirst = true; public bool $isService = false; @@ -32,11 +32,6 @@ class Show extends Component 'host_path' => 'host', ]; - public function mount() - { - $this->modalId = new Cuid2(7); - } - public function submit() { $this->validate(); @@ -44,9 +39,17 @@ class Show extends Component $this->dispatch('success', 'Storage updated successfully'); } - public function delete() + public function delete($password) { + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); + + return; + } + } + $this->storage->delete(); - $this->dispatch('refresh_storages'); + $this->dispatch('refreshStorages'); } } diff --git a/app/Livewire/Project/Shared/Tags.php b/app/Livewire/Project/Shared/Tags.php index 85d5c21dc..dca6180ff 100644 --- a/app/Livewire/Project/Shared/Tags.php +++ b/app/Livewire/Project/Shared/Tags.php @@ -3,32 +3,63 @@ namespace App\Livewire\Project\Shared; use App\Models\Tag; +use Livewire\Attributes\Validate; use Livewire\Component; +// Refactored ✅ class Tags extends Component { public $resource = null; - public ?string $new_tag = null; + #[Validate('required|string|min:2')] + public string $newTags; public $tags = []; - protected $listeners = [ - 'refresh' => '$refresh', - ]; - - protected $rules = [ - 'resource.tags.*.name' => 'required|string|min:2', - 'new_tag' => 'required|string|min:2', - ]; - - protected $validationAttributes = [ - 'new_tag' => 'tag', - ]; + public $filteredTags = []; public function mount() + { + $this->loadTags(); + } + + public function loadTags() { $this->tags = Tag::ownedByCurrentTeam()->get(); + $this->filteredTags = $this->tags->filter(function ($tag) { + return ! $this->resource->tags->contains($tag); + }); + } + + public function submit() + { + try { + $this->validate(); + $tags = str($this->newTags)->trim()->explode(' '); + foreach ($tags as $tag) { + if (strlen($tag) < 2) { + $this->dispatch('error', 'Invalid tag.', "Tag $tag is invalid. Min length is 2."); + + continue; + } + if ($this->resource->tags()->where('name', $tag)->exists()) { + $this->dispatch('error', 'Duplicate tags.', "Tag $tag already added."); + + continue; + } + $found = Tag::ownedByCurrentTeam()->where(['name' => $tag])->exists(); + if (! $found) { + $found = Tag::create([ + 'name' => $tag, + 'team_id' => currentTeam()->id, + ]); + } + $this->resource->tags()->attach($found->id); + } + $this->refresh(); + } catch (\Exception $e) { + return handleError($e, $this); + } } public function addTag(string $id, string $name) @@ -39,8 +70,9 @@ class Tags extends Component return; } - $this->resource->tags()->syncWithoutDetaching($id); + $this->resource->tags()->attach($id); $this->refresh(); + $this->dispatch('success', 'Tag added.'); } catch (\Exception $e) { return handleError($e, $this); } @@ -50,12 +82,12 @@ class Tags extends Component { try { $this->resource->tags()->detach($id); - - $found_more_tags = Tag::where(['id' => $id, 'team_id' => currentTeam()->id])->first(); - if ($found_more_tags->applications()->count() == 0 && $found_more_tags->services()->count() == 0) { + $found_more_tags = Tag::ownedByCurrentTeam()->find($id); + if ($found_more_tags && $found_more_tags->applications()->count() == 0 && $found_more_tags->services()->count() == 0) { $found_more_tags->delete(); } $this->refresh(); + $this->dispatch('success', 'Tag deleted.'); } catch (\Exception $e) { return handleError($e, $this); } @@ -63,41 +95,8 @@ class Tags extends Component public function refresh() { - $this->resource->load(['tags']); - $this->tags = Tag::ownedByCurrentTeam()->get(); - $this->new_tag = null; - } - - public function submit() - { - try { - $this->validate([ - 'new_tag' => 'required|string|min:2', - ]); - $tags = str($this->new_tag)->trim()->explode(' '); - foreach ($tags as $tag) { - if ($this->resource->tags()->where('name', $tag)->exists()) { - $this->dispatch('error', 'Duplicate tags.', "Tag $tag already added."); - - continue; - } - $found = Tag::where(['name' => $tag, 'team_id' => currentTeam()->id])->first(); - if (! $found) { - $found = Tag::create([ - 'name' => $tag, - 'team_id' => currentTeam()->id, - ]); - } - $this->resource->tags()->syncWithoutDetaching($found->id); - } - $this->refresh(); - } catch (\Exception $e) { - return handleError($e, $this); - } - } - - public function render() - { - return view('livewire.project.shared.tags'); + $this->resource->refresh(); // Remove this when legacy_model_binding is false + $this->loadTags(); + $this->reset('newTags'); } } diff --git a/app/Livewire/Project/Shared/Terminal.php b/app/Livewire/Project/Shared/Terminal.php new file mode 100644 index 000000000..5af8f057e --- /dev/null +++ b/app/Livewire/Project/Shared/Terminal.php @@ -0,0 +1,57 @@ +user()->currentTeam()->id; + + return [ + "echo-private:team.{$teamId},ApplicationStatusChanged" => 'closeTerminal', + ]; + } + + public function closeTerminal() + { + $this->dispatch('reloadWindow'); + } + + #[On('send-terminal-command')] + public function sendTerminalCommand($isContainer, $identifier, $serverUuid) + { + $server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail(); + + if ($isContainer) { + $status = getContainerStatus($server, $identifier); + if ($status !== 'running') { + return; + } + $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); + } else { + $command = SshMultiplexingHelper::generateSshCommand($server, 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n "$SHELL" ]; then exec $SHELL; else sh; fi'); + } + + // ssh command is sent back to frontend then to websocket + // this is done because the websocket connection is not available here + // a better solution would be to remove websocket on NodeJS and work with something like + // 1. Laravel Pusher/Echo connection (not possible without a sdk) + // 2. Ratchet / Revolt / ReactPHP / Event Loop (possible but hard to implement and huge dependencies) + // 3. Just found out about this https://github.com/sirn-se/websocket-php, perhaps it can be used + // 4. Follow-up discussions here: + // - https://github.com/coollabsio/coolify/issues/2298 + // - https://github.com/coollabsio/coolify/discussions/3362 + $this->dispatch('send-back-command', $command); + } + + public function render() + { + return view('livewire.project.shared.terminal'); + } +} diff --git a/app/Livewire/Project/Shared/UploadConfig.php b/app/Livewire/Project/Shared/UploadConfig.php new file mode 100644 index 000000000..1b10f588b --- /dev/null +++ b/app/Livewire/Project/Shared/UploadConfig.php @@ -0,0 +1,46 @@ +config = '{ + "build_pack": "nixpacks", + "base_directory": "/nodejs", + "publish_directory": "/", + "ports_exposes": "3000", + "settings": { + "is_static": false + } +}'; + } + } + + public function uploadConfig() + { + try { + $application = Application::findOrFail($this->applicationId); + $application->setConfig($this->config); + $this->dispatch('success', 'Application settings updated'); + } catch (\Exception $e) { + $this->dispatch('error', $e->getMessage()); + + return; + } + } + + public function render() + { + return view('livewire.project.shared.upload-config'); + } +} diff --git a/app/Livewire/Project/Shared/Webhooks.php b/app/Livewire/Project/Shared/Webhooks.php index e96bd888e..aab1fdc47 100644 --- a/app/Livewire/Project/Shared/Webhooks.php +++ b/app/Livewire/Project/Shared/Webhooks.php @@ -4,49 +4,61 @@ namespace App\Livewire\Project\Shared; use Livewire\Component; +// Refactored ✅ class Webhooks extends Component { public $resource; - public ?string $deploywebhook = null; + public ?string $deploywebhook; - public ?string $githubManualWebhook = null; + public ?string $githubManualWebhook; - public ?string $gitlabManualWebhook = null; + public ?string $gitlabManualWebhook; - public ?string $bitbucketManualWebhook = null; + public ?string $bitbucketManualWebhook; - public ?string $giteaManualWebhook = null; + public ?string $giteaManualWebhook; - protected $rules = [ - 'resource.manual_webhook_secret_github' => 'nullable|string', - 'resource.manual_webhook_secret_gitlab' => 'nullable|string', - 'resource.manual_webhook_secret_bitbucket' => 'nullable|string', - 'resource.manual_webhook_secret_gitea' => 'nullable|string', - ]; + public ?string $githubManualWebhookSecret = null; - public function saveSecret() + public ?string $gitlabManualWebhookSecret = null; + + public ?string $bitbucketManualWebhookSecret = null; + + public ?string $giteaManualWebhookSecret = null; + + public function mount() + { + // ray()->clearAll(); + // ray()->showQueries(); + $this->deploywebhook = generateDeployWebhook($this->resource); + + $this->githubManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_github'); + $this->githubManualWebhook = generateGitManualWebhook($this->resource, 'github'); + + $this->gitlabManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitlab'); + $this->gitlabManualWebhook = generateGitManualWebhook($this->resource, 'gitlab'); + + $this->bitbucketManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_bitbucket'); + $this->bitbucketManualWebhook = generateGitManualWebhook($this->resource, 'bitbucket'); + + $this->giteaManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitea'); + $this->giteaManualWebhook = generateGitManualWebhook($this->resource, 'gitea'); + } + + public function submit() { try { - $this->validate(); - $this->resource->save(); + $this->authorize('update', $this->resource); + $this->resource->update([ + 'manual_webhook_secret_github' => $this->githubManualWebhookSecret, + 'manual_webhook_secret_gitlab' => $this->gitlabManualWebhookSecret, + 'manual_webhook_secret_bitbucket' => $this->bitbucketManualWebhookSecret, + 'manual_webhook_secret_gitea' => $this->giteaManualWebhookSecret, + ]); $this->dispatch('success', 'Secret Saved.'); } catch (\Exception $e) { return handleError($e, $this); } } - - public function mount() - { - $this->deploywebhook = generateDeployWebhook($this->resource); - $this->githubManualWebhook = generateGitManualWebhook($this->resource, 'github'); - $this->gitlabManualWebhook = generateGitManualWebhook($this->resource, 'gitlab'); - $this->bitbucketManualWebhook = generateGitManualWebhook($this->resource, 'bitbucket'); - $this->giteaManualWebhook = generateGitManualWebhook($this->resource, 'gitea'); - } - - public function render() - { - return view('livewire.project.shared.webhooks'); - } } diff --git a/app/Livewire/Project/Show.php b/app/Livewire/Project/Show.php index d5d660017..2335519c7 100644 --- a/app/Livewire/Project/Show.php +++ b/app/Livewire/Project/Show.php @@ -2,24 +2,46 @@ namespace App\Livewire\Project; +use App\Models\Environment; use App\Models\Project; +use Livewire\Attributes\Validate; use Livewire\Component; class Show extends Component { public Project $project; - public function mount() - { - $projectUuid = request()->route('project_uuid'); - $teamId = currentTeam()->id; + #[Validate(['required', 'string', 'min:3'])] + public string $name; - $project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first(); - if (! $project) { - return redirect()->route('dashboard'); + #[Validate(['nullable', 'string'])] + public ?string $description = null; + + public function mount(string $project_uuid) + { + try { + $this->project = Project::where('team_id', currentTeam()->id)->where('uuid', $project_uuid)->firstOrFail(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function submit() + { + try { + $this->validate(); + $environment = Environment::create([ + 'name' => $this->name, + 'project_id' => $this->project->id, + ]); + + return redirect()->route('project.resource.index', [ + 'project_uuid' => $this->project->uuid, + 'environment_name' => $environment->name, + ]); + } catch (\Throwable $e) { + handleError($e, $this); } - $project->load(['environments']); - $this->project = $project; } public function render() diff --git a/app/Livewire/RunCommand.php b/app/Livewire/RunCommand.php deleted file mode 100644 index fc7f1eefc..000000000 --- a/app/Livewire/RunCommand.php +++ /dev/null @@ -1,42 +0,0 @@ - 'required', - 'command' => 'required', - ]; - - protected $validationAttributes = [ - 'server' => 'server', - 'command' => 'command', - ]; - - public function mount($servers) - { - $this->servers = $servers; - $this->server = $servers[0]->uuid; - } - - public function runCommand() - { - $this->validate(); - try { - $activity = remote_process([$this->command], Server::where('uuid', $this->server)->first(), ignore_errors: true); - $this->dispatch('activityMonitor', $activity->id); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } -} diff --git a/app/Livewire/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php index c485a6a3a..fe68a8ba5 100644 --- a/app/Livewire/Security/ApiTokens.php +++ b/app/Livewire/Security/ApiTokens.php @@ -2,6 +2,7 @@ namespace App\Livewire\Security; +use App\Models\InstanceSettings; use Livewire\Component; class ApiTokens extends Component @@ -10,6 +11,16 @@ class ApiTokens extends Component public $tokens = []; + public bool $viewSensitiveData = false; + + public bool $readOnly = true; + + public bool $rootAccess = false; + + public array $permissions = ['read-only']; + + public $isApiEnabled; + public function render() { return view('livewire.security.api-tokens'); @@ -17,7 +28,52 @@ class ApiTokens extends Component public function mount() { - $this->tokens = auth()->user()->tokens; + $this->isApiEnabled = InstanceSettings::get()->is_api_enabled; + $this->tokens = auth()->user()->tokens->sortByDesc('created_at'); + } + + public function updatedViewSensitiveData() + { + if ($this->viewSensitiveData) { + $this->permissions[] = 'view:sensitive'; + $this->permissions = array_diff($this->permissions, ['*']); + $this->rootAccess = false; + } else { + $this->permissions = array_diff($this->permissions, ['view:sensitive']); + } + $this->makeSureOneIsSelected(); + } + + public function updatedReadOnly() + { + if ($this->readOnly) { + $this->permissions[] = 'read-only'; + $this->permissions = array_diff($this->permissions, ['*']); + $this->rootAccess = false; + } else { + $this->permissions = array_diff($this->permissions, ['read-only']); + } + $this->makeSureOneIsSelected(); + } + + public function updatedRootAccess() + { + if ($this->rootAccess) { + $this->permissions = ['*']; + $this->readOnly = false; + $this->viewSensitiveData = false; + } else { + $this->readOnly = true; + $this->permissions = ['read-only']; + } + } + + public function makeSureOneIsSelected() + { + if (count($this->permissions) == 0) { + $this->permissions = ['read-only']; + $this->readOnly = true; + } } public function addNewToken() @@ -26,7 +82,7 @@ class ApiTokens extends Component $this->validate([ 'description' => 'required|min:3|max:255', ]); - $token = auth()->user()->createToken($this->description); + $token = auth()->user()->createToken($this->description, $this->permissions); $this->tokens = auth()->user()->tokens; session()->flash('token', $token->plainTextToken); } catch (\Exception $e) { diff --git a/app/Livewire/Security/PrivateKey/Create.php b/app/Livewire/Security/PrivateKey/Create.php index 32a67bbea..319cec192 100644 --- a/app/Livewire/Security/PrivateKey/Create.php +++ b/app/Livewire/Security/PrivateKey/Create.php @@ -3,17 +3,13 @@ namespace App\Livewire\Security\PrivateKey; use App\Models\PrivateKey; -use DanHarrin\LivewireRateLimiting\WithRateLimiting; use Livewire\Component; -use phpseclib3\Crypt\PublicKeyLoader; class Create extends Component { - use WithRateLimiting; + public string $name = ''; - public string $name; - - public string $value; + public string $value = ''; public ?string $from = null; @@ -26,72 +22,69 @@ class Create extends Component 'value' => 'required|string', ]; - protected $validationAttributes = [ - 'name' => 'name', - 'value' => 'private Key', - ]; - public function generateNewRSAKey() { - try { - $this->rateLimit(10); - $this->name = generate_random_name(); - $this->description = 'Created by Coolify'; - ['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey(); - } catch (\Throwable $e) { - return handleError($e, $this); - } + $this->generateNewKey('rsa'); } public function generateNewEDKey() { - try { - $this->rateLimit(10); - $this->name = generate_random_name(); - $this->description = 'Created by Coolify'; - ['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey('ed25519'); - } catch (\Throwable $e) { - return handleError($e, $this); - } + $this->generateNewKey('ed25519'); } - public function updated($updateProperty) + private function generateNewKey($type) { - if ($updateProperty === 'value') { - try { - $this->publicKey = PublicKeyLoader::load($this->$updateProperty)->getPublicKey()->toString('OpenSSH', ['comment' => '']); - } catch (\Throwable $e) { - if ($this->$updateProperty === '') { - $this->publicKey = ''; - } else { - $this->publicKey = 'Invalid private key'; - } - } + $keyData = PrivateKey::generateNewKeyPair($type); + $this->setKeyData($keyData); + } + + public function updated($property) + { + if ($property === 'value') { + $this->validatePrivateKey(); } - $this->validateOnly($updateProperty); } public function createPrivateKey() { $this->validate(); + try { - $this->value = trim($this->value); - if (! str_ends_with($this->value, "\n")) { - $this->value .= "\n"; - } - $private_key = PrivateKey::create([ + $privateKey = PrivateKey::createAndStore([ 'name' => $this->name, 'description' => $this->description, - 'private_key' => $this->value, + 'private_key' => trim($this->value)."\n", 'team_id' => currentTeam()->id, ]); - if ($this->from === 'server') { - return redirect()->route('dashboard'); - } - return redirect()->route('security.private-key.show', ['private_key_uuid' => $private_key->uuid]); + return $this->redirectAfterCreation($privateKey); } catch (\Throwable $e) { return handleError($e, $this); } } + + private function setKeyData(array $keyData) + { + $this->name = $keyData['name']; + $this->description = $keyData['description']; + $this->value = $keyData['private_key']; + $this->publicKey = $keyData['public_key']; + } + + private function validatePrivateKey() + { + $validationResult = PrivateKey::validateAndExtractPublicKey($this->value); + $this->publicKey = $validationResult['publicKey']; + + if (! $validationResult['isValid']) { + $this->addError('value', 'Invalid private key'); + } + } + + private function redirectAfterCreation(PrivateKey $privateKey) + { + return $this->from === 'server' + ? redirect()->route('dashboard') + : redirect()->route('security.private-key.show', ['private_key_uuid' => $privateKey->uuid]); + } } diff --git a/app/Livewire/Security/PrivateKey/Index.php b/app/Livewire/Security/PrivateKey/Index.php new file mode 100644 index 000000000..76441a67e --- /dev/null +++ b/app/Livewire/Security/PrivateKey/Index.php @@ -0,0 +1,24 @@ +get(); + + return view('livewire.security.private-key.index', [ + 'privateKeys' => $privateKeys, + ])->layout('components.layout'); + } + + public function cleanupUnusedKeys() + { + PrivateKey::cleanupUnusedKeys(); + $this->dispatch('success', 'Unused keys have been cleaned up.'); + } +} diff --git a/app/Livewire/Security/PrivateKey/Show.php b/app/Livewire/Security/PrivateKey/Show.php index d86bd5d1e..b9195b543 100644 --- a/app/Livewire/Security/PrivateKey/Show.php +++ b/app/Livewire/Security/PrivateKey/Show.php @@ -28,26 +28,28 @@ class Show extends Component { try { $this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail(); - } catch (\Throwable $e) { - return handleError($e, $this); + } catch (\Throwable) { + abort(404); } } public function loadPublicKey() { - $this->public_key = $this->private_key->publicKey(); + $this->public_key = $this->private_key->getPublicKey(); + if ($this->public_key === 'Error loading private key') { + $this->dispatch('error', 'Failed to load public key. The private key may be invalid.'); + } } public function delete() { try { - if ($this->private_key->isEmpty()) { - $this->private_key->delete(); - currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get(); + $this->private_key->safeDelete(); + currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get(); - return redirect()->route('security.private-key.index'); - } - $this->dispatch('error', 'This private key is in use and cannot be deleted. Please delete all servers, applications, and GitHub/GitLab apps that use this private key before deleting it.'); + return redirect()->route('security.private-key.index'); + } catch (\Exception $e) { + $this->dispatch('error', $e->getMessage()); } catch (\Throwable $e) { return handleError($e, $this); } @@ -56,8 +58,9 @@ class Show extends Component public function changePrivateKey() { try { - $this->private_key->private_key = formatPrivateKey($this->private_key->private_key); - $this->private_key->save(); + $this->private_key->updatePrivateKey([ + 'private_key' => formatPrivateKey($this->private_key->private_key), + ]); refresh_server_connection($this->private_key); $this->dispatch('success', 'Private key updated.'); } catch (\Throwable $e) { diff --git a/app/Livewire/Server/Advanced.php b/app/Livewire/Server/Advanced.php new file mode 100644 index 000000000..0852abebf --- /dev/null +++ b/app/Livewire/Server/Advanced.php @@ -0,0 +1,115 @@ +server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + $this->parameters = get_route_parameters(); + $this->syncData(); + } catch (\Throwable) { + return redirect()->route('server.show'); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->server->settings->concurrent_builds = $this->concurrentBuilds; + $this->server->settings->dynamic_timeout = $this->dynamicTimeout; + $this->server->settings->force_docker_cleanup = $this->forceDockerCleanup; + $this->server->settings->docker_cleanup_frequency = $this->dockerCleanupFrequency; + $this->server->settings->docker_cleanup_threshold = $this->dockerCleanupThreshold; + $this->server->settings->server_disk_usage_notification_threshold = $this->serverDiskUsageNotificationThreshold; + $this->server->settings->delete_unused_volumes = $this->deleteUnusedVolumes; + $this->server->settings->delete_unused_networks = $this->deleteUnusedNetworks; + $this->server->settings->save(); + } else { + $this->concurrentBuilds = $this->server->settings->concurrent_builds; + $this->dynamicTimeout = $this->server->settings->dynamic_timeout; + $this->forceDockerCleanup = $this->server->settings->force_docker_cleanup; + $this->dockerCleanupFrequency = $this->server->settings->docker_cleanup_frequency; + $this->dockerCleanupThreshold = $this->server->settings->docker_cleanup_threshold; + $this->serverDiskUsageNotificationThreshold = $this->server->settings->server_disk_usage_notification_threshold; + $this->deleteUnusedVolumes = $this->server->settings->delete_unused_volumes; + $this->deleteUnusedNetworks = $this->server->settings->delete_unused_networks; + } + } + + public function instantSave() + { + try { + $this->syncData(true); + $this->dispatch('success', 'Server updated.'); + // $this->dispatch('refreshServerShow'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function manualCleanup() + { + try { + DockerCleanupJob::dispatch($this->server, true); + $this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function submit() + { + try { + if (! validate_cron_expression($this->dockerCleanupFrequency)) { + $this->dockerCleanupFrequency = $this->server->settings->getOriginal('docker_cleanup_frequency'); + throw new \Exception('Invalid Cron / Human expression for Docker Cleanup Frequency.'); + } + $this->syncData(true); + $this->dispatch('success', 'Server updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.server.advanced'); + } +} diff --git a/app/Livewire/Server/Charts.php b/app/Livewire/Server/Charts.php new file mode 100644 index 000000000..d0db87f57 --- /dev/null +++ b/app/Livewire/Server/Charts.php @@ -0,0 +1,64 @@ +server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function pollData() + { + if ($this->poll || $this->interval <= 10) { + $this->loadData(); + if ($this->interval > 10) { + $this->poll = false; + } + } + } + + public function loadData() + { + try { + $cpuMetrics = $this->server->getCpuMetrics($this->interval); + $memoryMetrics = $this->server->getMemoryMetrics($this->interval); + $this->dispatch("refreshChartData-{$this->chartId}-cpu", [ + 'seriesData' => $cpuMetrics, + ]); + $this->dispatch("refreshChartData-{$this->chartId}-memory", [ + 'seriesData' => $memoryMetrics, + ]); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function setInterval() + { + if ($this->interval <= 10) { + $this->poll = true; + } + $this->loadData(); + } +} diff --git a/app/Livewire/Server/CloudflareTunnels.php b/app/Livewire/Server/CloudflareTunnels.php new file mode 100644 index 000000000..f69fc8655 --- /dev/null +++ b/app/Livewire/Server/CloudflareTunnels.php @@ -0,0 +1,54 @@ +server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + if ($this->server->isLocalhost()) { + return redirect()->route('server.show', ['server_uuid' => $server_uuid]); + } + $this->isCloudflareTunnelsEnabled = $this->server->settings->is_cloudflare_tunnel; + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function instantSave() + { + try { + $this->validate(); + $this->server->settings->is_cloudflare_tunnel = $this->isCloudflareTunnelsEnabled; + $this->server->settings->save(); + $this->dispatch('success', 'Server updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function manualCloudflareConfig() + { + $this->isCloudflareTunnelsEnabled = true; + $this->server->settings->is_cloudflare_tunnel = true; + $this->server->settings->save(); + $this->server->refresh(); + $this->dispatch('success', 'Cloudflare Tunnels enabled.'); + } + + public function render() + { + return view('livewire.server.cloudflare-tunnels'); + } +} diff --git a/app/Livewire/Server/ConfigureCloudflareTunnels.php b/app/Livewire/Server/ConfigureCloudflareTunnels.php index 7d2103e37..f58d7b6be 100644 --- a/app/Livewire/Server/ConfigureCloudflareTunnels.php +++ b/app/Livewire/Server/ConfigureCloudflareTunnels.php @@ -21,7 +21,7 @@ class ConfigureCloudflareTunnels extends Component $server->settings->is_cloudflare_tunnel = true; $server->settings->save(); $this->dispatch('success', 'Cloudflare Tunnels configured successfully.'); - $this->dispatch('serverInstalled'); + $this->dispatch('refreshServerShow'); } catch (\Throwable $e) { return handleError($e, $this); } @@ -30,14 +30,18 @@ class ConfigureCloudflareTunnels extends Component public function submit() { try { + if (str($this->ssh_domain)->contains('https://')) { + $this->ssh_domain = str($this->ssh_domain)->replace('https://', '')->replace('http://', '')->trim(); + // remove / from the end + $this->ssh_domain = str($this->ssh_domain)->replace('/', ''); + } $server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail(); - ConfigureCloudflared::run($server, $this->cloudflare_token); + ConfigureCloudflared::dispatch($server, $this->cloudflare_token); $server->settings->is_cloudflare_tunnel = true; $server->ip = $this->ssh_domain; $server->save(); $server->settings->save(); - $this->dispatch('success', 'Cloudflare Tunnels configured successfully.'); - $this->dispatch('serverInstalled'); + $this->dispatch('warning', 'Cloudflare Tunnels configuration started.'); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/Delete.php b/app/Livewire/Server/Delete.php index 3beec0c91..b9e3944b5 100644 --- a/app/Livewire/Server/Delete.php +++ b/app/Livewire/Server/Delete.php @@ -2,17 +2,38 @@ namespace App\Livewire\Server; +use App\Actions\Server\DeleteServer; +use App\Models\InstanceSettings; +use App\Models\Server; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; use Livewire\Component; class Delete extends Component { use AuthorizesRequests; - public $server; + public Server $server; - public function delete() + public function mount(string $server_uuid) { + try { + $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function delete($password) + { + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); + + return; + } + } try { $this->authorize('delete', $this->server); if ($this->server->hasDefinedResources()) { @@ -21,6 +42,7 @@ class Delete extends Component return; } $this->server->delete(); + DeleteServer::dispatch($this->server); return redirect()->route('server.index'); } catch (\Throwable $e) { diff --git a/app/Livewire/Server/Destination/Show.php b/app/Livewire/Server/Destination/Show.php deleted file mode 100644 index 986e16cbf..000000000 --- a/app/Livewire/Server/Destination/Show.php +++ /dev/null @@ -1,31 +0,0 @@ -parameters = get_route_parameters(); - try { - $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); - if (is_null($this->server)) { - return redirect()->route('server.index'); - } - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function render() - { - return view('livewire.server.destination.show'); - } -} diff --git a/app/Livewire/Server/Destinations.php b/app/Livewire/Server/Destinations.php new file mode 100644 index 000000000..dbab6e03f --- /dev/null +++ b/app/Livewire/Server/Destinations.php @@ -0,0 +1,90 @@ +networks = collect(); + $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + private function createNetworkAndAttachToProxy() + { + $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + } + + public function add($name) + { + if ($this->server->isSwarm()) { + $found = $this->server->swarmDockers()->where('network', $name)->first(); + if ($found) { + $this->dispatch('error', 'Network already added to this server.'); + + return; + } else { + SwarmDocker::create([ + 'name' => $this->server->name.'-'.$name, + 'network' => $this->name, + 'server_id' => $this->server->id, + ]); + } + } else { + $found = $this->server->standaloneDockers()->where('network', $name)->first(); + if ($found) { + $this->dispatch('error', 'Network already added to this server.'); + + return; + } else { + StandaloneDocker::create([ + 'name' => $this->server->name.'-'.$name, + 'network' => $name, + 'server_id' => $this->server->id, + ]); + } + $this->createNetworkAndAttachToProxy(); + } + } + + public function scan() + { + if ($this->server->isSwarm()) { + $alreadyAddedNetworks = $this->server->swarmDockers; + } else { + $alreadyAddedNetworks = $this->server->standaloneDockers; + } + $networks = instant_remote_process(['docker network ls --format "{{json .}}"'], $this->server, false); + $this->networks = format_docker_command_output_to_json($networks)->filter(function ($network) { + return $network['Name'] !== 'bridge' && $network['Name'] !== 'host' && $network['Name'] !== 'none'; + })->filter(function ($network) use ($alreadyAddedNetworks) { + return ! $alreadyAddedNetworks->contains('network', $network['Name']); + }); + if ($this->networks->count() === 0) { + $this->dispatch('success', 'No new destinations found on this server.'); + + return; + } + $this->dispatch('success', 'Scan done.'); + } + + public function render() + { + return view('livewire.server.destinations'); + } +} diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php deleted file mode 100644 index 263ff6367..000000000 --- a/app/Livewire/Server/Form.php +++ /dev/null @@ -1,141 +0,0 @@ - '$refresh']; - - protected $rules = [ - 'server.name' => 'required', - 'server.description' => 'nullable', - 'server.ip' => 'required', - 'server.user' => 'required', - 'server.port' => 'required', - 'server.settings.is_cloudflare_tunnel' => 'required|boolean', - 'server.settings.is_reachable' => 'required', - 'server.settings.is_swarm_manager' => 'required|boolean', - 'server.settings.is_swarm_worker' => 'required|boolean', - 'server.settings.is_build_server' => 'required|boolean', - 'server.settings.concurrent_builds' => 'required|integer|min:1', - 'server.settings.dynamic_timeout' => 'required|integer|min:1', - 'wildcard_domain' => 'nullable|url', - ]; - - protected $validationAttributes = [ - 'server.name' => 'Name', - 'server.description' => 'Description', - 'server.ip' => 'IP address/Domain', - 'server.user' => 'User', - 'server.port' => 'Port', - 'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel', - 'server.settings.is_reachable' => 'Is reachable', - 'server.settings.is_swarm_manager' => 'Swarm Manager', - 'server.settings.is_swarm_worker' => 'Swarm Worker', - 'server.settings.is_build_server' => 'Build Server', - 'server.settings.concurrent_builds' => 'Concurrent Builds', - 'server.settings.dynamic_timeout' => 'Dynamic Timeout', - - ]; - - public function mount() - { - $this->wildcard_domain = $this->server->settings->wildcard_domain; - $this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage; - } - - public function serverInstalled() - { - $this->server->refresh(); - $this->server->settings->refresh(); - } - - public function updatedServerSettingsIsBuildServer() - { - $this->dispatch('serverInstalled'); - $this->dispatch('serverRefresh'); - $this->dispatch('proxyStatusUpdated'); - } - - public function instantSave() - { - try { - refresh_server_connection($this->server->privateKey); - $this->validateServer(false); - $this->server->settings->save(); - $this->dispatch('success', 'Server updated.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function revalidate() - { - $this->revalidate = true; - } - - public function checkLocalhostConnection() - { - $this->submit(); - ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(); - if ($uptime) { - $this->dispatch('success', 'Server is reachable.'); - $this->server->settings->is_reachable = true; - $this->server->settings->is_usable = true; - $this->server->settings->save(); - $this->dispatch('proxyStatusUpdated'); - } else { - $this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.

Check this documentation for further help.

Error: '.$error); - - return; - } - } - - public function validateServer($install = true) - { - $this->dispatch('init', $install); - } - - public function submit() - { - if (isCloud() && ! isDev()) { - $this->validate(); - $this->validate([ - 'server.ip' => 'required', - ]); - } else { - $this->validate(); - } - $uniqueIPs = Server::all()->reject(function (Server $server) { - return $server->id === $this->server->id; - })->pluck('ip')->toArray(); - if (in_array($this->server->ip, $uniqueIPs)) { - $this->dispatch('error', 'IP address is already in use by another team.'); - - return; - } - refresh_server_connection($this->server->privateKey); - $this->server->settings->wildcard_domain = $this->wildcard_domain; - $this->server->settings->cleanup_after_percentage = $this->cleanup_after_percentage; - $this->server->settings->save(); - $this->server->save(); - $this->dispatch('success', 'Server updated.'); - } -} diff --git a/app/Livewire/Server/LogDrains.php b/app/Livewire/Server/LogDrains.php index 3d7b34de1..6599149c4 100644 --- a/app/Livewire/Server/LogDrains.php +++ b/app/Livewire/Server/LogDrains.php @@ -2,83 +2,132 @@ namespace App\Livewire\Server; -use App\Actions\Server\InstallLogDrain; +use App\Actions\Server\StartLogDrain; +use App\Actions\Server\StopLogDrain; use App\Models\Server; +use Livewire\Attributes\Validate; use Livewire\Component; class LogDrains extends Component { public Server $server; - public $parameters = []; + #[Validate(['boolean'])] + public bool $isLogDrainNewRelicEnabled = false; - protected $rules = [ - 'server.settings.is_logdrain_newrelic_enabled' => 'required|boolean', - 'server.settings.logdrain_newrelic_license_key' => 'required|string', - 'server.settings.logdrain_newrelic_base_uri' => 'required|string', - 'server.settings.is_logdrain_highlight_enabled' => 'required|boolean', - 'server.settings.logdrain_highlight_project_id' => 'required|string', - 'server.settings.is_logdrain_axiom_enabled' => 'required|boolean', - 'server.settings.logdrain_axiom_dataset_name' => 'required|string', - 'server.settings.logdrain_axiom_api_key' => 'required|string', - 'server.settings.is_logdrain_custom_enabled' => 'required|boolean', - 'server.settings.logdrain_custom_config' => 'required|string', - 'server.settings.logdrain_custom_config_parser' => 'nullable', - ]; + #[Validate(['boolean'])] + public bool $isLogDrainCustomEnabled = false; - protected $validationAttributes = [ - 'server.settings.is_logdrain_newrelic_enabled' => 'New Relic log drain', - 'server.settings.logdrain_newrelic_license_key' => 'New Relic license key', - 'server.settings.logdrain_newrelic_base_uri' => 'New Relic base URI', - 'server.settings.is_logdrain_highlight_enabled' => 'Highlight log drain', - 'server.settings.logdrain_highlight_project_id' => 'Highlight project ID', - 'server.settings.is_logdrain_axiom_enabled' => 'Axiom log drain', - 'server.settings.logdrain_axiom_dataset_name' => 'Axiom dataset name', - 'server.settings.logdrain_axiom_api_key' => 'Axiom API key', - 'server.settings.is_logdrain_custom_enabled' => 'Custom log drain', - 'server.settings.logdrain_custom_config' => 'Custom log drain configuration', - 'server.settings.logdrain_custom_config_parser' => 'Custom log drain configuration parser', - ]; + #[Validate(['boolean'])] + public bool $isLogDrainAxiomEnabled = false; - public function mount() + #[Validate(['string', 'nullable'])] + public ?string $logDrainNewRelicLicenseKey = null; + + #[Validate(['url', 'nullable'])] + public ?string $logDrainNewRelicBaseUri = null; + + #[Validate(['string', 'nullable'])] + public ?string $logDrainAxiomDatasetName = null; + + #[Validate(['string', 'nullable'])] + public ?string $logDrainAxiomApiKey = null; + + #[Validate(['string', 'nullable'])] + public ?string $logDrainCustomConfig = null; + + #[Validate(['string', 'nullable'])] + public ?string $logDrainCustomConfigParser = null; + + public function mount(string $server_uuid) { - $this->parameters = get_route_parameters(); try { - $server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); - if (is_null($server)) { - return redirect()->route('server.index'); - } - $this->server = $server; + $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + $this->syncData(); } catch (\Throwable $e) { return handleError($e, $this); } } - public function configureLogDrain() + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->customValidation(); + $this->server->settings->is_logdrain_newrelic_enabled = $this->isLogDrainNewRelicEnabled; + $this->server->settings->is_logdrain_axiom_enabled = $this->isLogDrainAxiomEnabled; + $this->server->settings->is_logdrain_custom_enabled = $this->isLogDrainCustomEnabled; + + $this->server->settings->logdrain_newrelic_license_key = $this->logDrainNewRelicLicenseKey; + $this->server->settings->logdrain_newrelic_base_uri = $this->logDrainNewRelicBaseUri; + $this->server->settings->logdrain_axiom_dataset_name = $this->logDrainAxiomDatasetName; + $this->server->settings->logdrain_axiom_api_key = $this->logDrainAxiomApiKey; + $this->server->settings->logdrain_custom_config = $this->logDrainCustomConfig; + $this->server->settings->logdrain_custom_config_parser = $this->logDrainCustomConfigParser; + + $this->server->settings->save(); + } else { + $this->isLogDrainNewRelicEnabled = $this->server->settings->is_logdrain_newrelic_enabled; + $this->isLogDrainAxiomEnabled = $this->server->settings->is_logdrain_axiom_enabled; + $this->isLogDrainCustomEnabled = $this->server->settings->is_logdrain_custom_enabled; + + $this->logDrainNewRelicLicenseKey = $this->server->settings->logdrain_newrelic_license_key; + $this->logDrainNewRelicBaseUri = $this->server->settings->logdrain_newrelic_base_uri; + $this->logDrainAxiomDatasetName = $this->server->settings->logdrain_axiom_dataset_name; + $this->logDrainAxiomApiKey = $this->server->settings->logdrain_axiom_api_key; + $this->logDrainCustomConfig = $this->server->settings->logdrain_custom_config; + $this->logDrainCustomConfigParser = $this->server->settings->logdrain_custom_config_parser; + } + } + + public function customValidation() + { + if ($this->isLogDrainNewRelicEnabled) { + try { + $this->validate([ + 'logDrainNewRelicLicenseKey' => ['required'], + 'logDrainNewRelicBaseUri' => ['required', 'url'], + ]); + } catch (\Throwable $e) { + $this->isLogDrainNewRelicEnabled = false; + + throw $e; + } + } elseif ($this->isLogDrainAxiomEnabled) { + try { + $this->validate([ + 'logDrainAxiomDatasetName' => ['required'], + 'logDrainAxiomApiKey' => ['required'], + ]); + } catch (\Throwable $e) { + $this->isLogDrainAxiomEnabled = false; + + throw $e; + } + } elseif ($this->isLogDrainCustomEnabled) { + try { + $this->validate([ + 'logDrainCustomConfig' => ['required'], + 'logDrainCustomConfigParser' => ['string', 'nullable'], + ]); + } catch (\Throwable $e) { + $this->isLogDrainCustomEnabled = false; + + throw $e; + } + } + } + + public function instantSave() { try { - InstallLogDrain::run($this->server); - if (! $this->server->isLogDrainEnabled()) { - $this->dispatch('serverRefresh'); + $this->syncData(true); + if ($this->server->isLogDrainEnabled()) { + StartLogDrain::run($this->server); + $this->dispatch('success', 'Log drain service started.'); + } else { + StopLogDrain::run($this->server); $this->dispatch('success', 'Log drain service stopped.'); - - return; } - $this->dispatch('serverRefresh'); - $this->dispatch('success', 'Log drain service started.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function instantSave(string $type) - { - try { - $ok = $this->submit($type); - if (! $ok) { - return; - } - $this->configureLogDrain(); } catch (\Throwable $e) { return handleError($e, $this); } @@ -87,76 +136,10 @@ class LogDrains extends Component public function submit(string $type) { try { - $this->resetErrorBag(); - if ($type === 'newrelic') { - $this->validate([ - 'server.settings.is_logdrain_newrelic_enabled' => 'required|boolean', - 'server.settings.logdrain_newrelic_license_key' => 'required|string', - 'server.settings.logdrain_newrelic_base_uri' => 'required|string', - ]); - $this->server->settings->update([ - 'is_logdrain_highlight_enabled' => false, - 'is_logdrain_axiom_enabled' => false, - 'is_logdrain_custom_enabled' => false, - ]); - } elseif ($type === 'highlight') { - $this->validate([ - 'server.settings.is_logdrain_highlight_enabled' => 'required|boolean', - 'server.settings.logdrain_highlight_project_id' => 'required|string', - ]); - $this->server->settings->update([ - 'is_logdrain_newrelic_enabled' => false, - 'is_logdrain_axiom_enabled' => false, - 'is_logdrain_custom_enabled' => false, - ]); - } elseif ($type === 'axiom') { - $this->validate([ - 'server.settings.is_logdrain_axiom_enabled' => 'required|boolean', - 'server.settings.logdrain_axiom_dataset_name' => 'required|string', - 'server.settings.logdrain_axiom_api_key' => 'required|string', - ]); - $this->server->settings->update([ - 'is_logdrain_newrelic_enabled' => false, - 'is_logdrain_highlight_enabled' => false, - 'is_logdrain_custom_enabled' => false, - ]); - } elseif ($type === 'custom') { - $this->validate([ - 'server.settings.is_logdrain_custom_enabled' => 'required|boolean', - 'server.settings.logdrain_custom_config' => 'required|string', - 'server.settings.logdrain_custom_config_parser' => 'nullable', - ]); - $this->server->settings->update([ - 'is_logdrain_newrelic_enabled' => false, - 'is_logdrain_highlight_enabled' => false, - 'is_logdrain_axiom_enabled' => false, - ]); - } - $this->server->settings->save(); + $this->syncData(true); $this->dispatch('success', 'Settings saved.'); - - return true; } catch (\Throwable $e) { - if ($type === 'newrelic') { - $this->server->settings->update([ - 'is_logdrain_newrelic_enabled' => false, - ]); - } elseif ($type === 'highlight') { - $this->server->settings->update([ - 'is_logdrain_highlight_enabled' => false, - ]); - } elseif ($type === 'axiom') { - $this->server->settings->update([ - 'is_logdrain_axiom_enabled' => false, - ]); - } elseif ($type === 'custom') { - $this->server->settings->update([ - 'is_logdrain_custom_enabled' => false, - ]); - } - handleError($e, $this); - - return false; + return handleError($e, $this); } } diff --git a/app/Livewire/Server/New/ByIp.php b/app/Livewire/Server/New/ByIp.php index 0aad33b1c..f80152435 100644 --- a/app/Livewire/Server/New/ByIp.php +++ b/app/Livewire/Server/New/ByIp.php @@ -2,10 +2,10 @@ namespace App\Livewire\Server\New; -use App\Enums\ProxyStatus; use App\Enums\ProxyTypes; use App\Models\Server; use App\Models\Team; +use Illuminate\Support\Collection; use Livewire\Component; class ByIp extends Component @@ -40,7 +40,7 @@ class ByIp extends Component public bool $is_build_server = false; - public $swarm_managers = []; + public Collection $swarm_managers; protected $rules = [ 'name' => 'required|string', @@ -102,11 +102,6 @@ class ByIp extends Component 'port' => $this->port, 'team_id' => currentTeam()->id, 'private_key_id' => $this->private_key_id, - 'proxy' => [ - // set default proxy type to traefik v2 - 'type' => ProxyTypes::TRAEFIK_V2->value, - 'status' => ProxyStatus::EXITED->value, - ], ]; if ($this->is_swarm_worker) { $payload['swarm_cluster'] = $this->selected_swarm_cluster; @@ -115,6 +110,9 @@ class ByIp extends Component data_forget($payload, 'proxy'); } $server = Server::create($payload); + $server->proxy->set('status', 'exited'); + $server->proxy->set('type', ProxyTypes::TRAEFIK->value); + $server->save(); if ($this->is_build_server) { $this->is_swarm_manager = false; $this->is_swarm_worker = false; @@ -124,7 +122,6 @@ class ByIp extends Component } $server->settings->is_build_server = $this->is_build_server; $server->settings->save(); - $server->addInitialNetwork(); return redirect()->route('server.show', $server->uuid); } catch (\Throwable $e) { diff --git a/app/Livewire/Server/PrivateKey/Show.php b/app/Livewire/Server/PrivateKey/Show.php index 0ad820428..64aa1884b 100644 --- a/app/Livewire/Server/PrivateKey/Show.php +++ b/app/Livewire/Server/PrivateKey/Show.php @@ -8,26 +8,63 @@ use Livewire\Component; class Show extends Component { - public ?Server $server = null; + public Server $server; public $privateKeys = []; public $parameters = []; - public function mount() + public function mount(string $server_uuid) { - $this->parameters = get_route_parameters(); try { - $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); - if (is_null($this->server)) { - return redirect()->route('server.index'); - } + $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); $this->privateKeys = PrivateKey::ownedByCurrentTeam()->get()->where('is_git_related', false); } catch (\Throwable $e) { return handleError($e, $this); } } + public function setPrivateKey($privateKeyId) + { + $ownedPrivateKey = PrivateKey::ownedByCurrentTeam()->find($privateKeyId); + if (is_null($ownedPrivateKey)) { + $this->dispatch('error', 'You are not allowed to use this private key.'); + + return; + } + + $originalPrivateKeyId = $this->server->getOriginal('private_key_id'); + try { + $this->server->update(['private_key_id' => $privateKeyId]); + ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(justCheckingNewKey: true); + if ($uptime) { + $this->dispatch('success', 'Private key updated successfully.'); + } else { + throw new \Exception($error); + } + } catch (\Exception $e) { + $this->server->update(['private_key_id' => $originalPrivateKeyId]); + $this->server->validateConnection(); + $this->dispatch('error', $e->getMessage()); + } + } + + public function checkConnection() + { + try { + ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(); + if ($uptime) { + $this->dispatch('success', 'Server is reachable.'); + } else { + $this->dispatch('error', 'Server is not reachable.

Check this documentation for further help.

Error: '.$error); + + return; + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function render() { return view('livewire.server.private-key.show'); diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index 8d1ece1c6..94ea3509a 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -6,7 +6,6 @@ use App\Actions\Proxy\CheckConfiguration; use App\Actions\Proxy\SaveConfiguration; use App\Actions\Proxy\StartProxy; use App\Models\Server; -use Illuminate\Support\Str; use Livewire\Component; class Proxy extends Component @@ -21,6 +20,10 @@ class Proxy extends Component protected $listeners = ['proxyStatusUpdated', 'saveConfiguration' => 'submit']; + protected $rules = [ + 'server.settings.generate_exact_labels' => 'required|boolean', + ]; + public function mount() { $this->selectedProxy = $this->server->proxyType(); @@ -32,24 +35,36 @@ class Proxy extends Component $this->dispatch('refresh')->self(); } - public function change_proxy() + public function changeProxy() { $this->server->proxy = null; $this->server->save(); + $this->dispatch('proxyChanged'); } - public function select_proxy($proxy_type) + public function selectProxy($proxy_type) { $this->server->proxy->set('status', 'exited'); $this->server->proxy->set('type', $proxy_type); $this->server->save(); $this->selectedProxy = $this->server->proxy->type; - if ($this->selectedProxy !== 'NONE') { + if ($this->server->proxySet()) { StartProxy::run($this->server, false); } $this->dispatch('proxyStatusUpdated'); } + public function instantSave() + { + try { + $this->validate(); + $this->server->settings->save(); + $this->dispatch('success', 'Settings saved.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function submit() { try { @@ -79,12 +94,11 @@ class Proxy extends Component { try { $this->proxy_settings = CheckConfiguration::run($this->server); - if (Str::of($this->proxy_settings)->contains('--api.dashboard=true') && Str::of($this->proxy_settings)->contains('--api.insecure=true')) { + if (str($this->proxy_settings)->contains('--api.dashboard=true') && str($this->proxy_settings)->contains('--api.insecure=true')) { $this->dispatch('traefikDashboardAvailable', true); } else { $this->dispatch('traefikDashboardAvailable', false); } - } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/Proxy/Deploy.php b/app/Livewire/Server/Proxy/Deploy.php index 6d3f00dc8..8fcff85d6 100644 --- a/app/Livewire/Server/Proxy/Deploy.php +++ b/app/Livewire/Server/Proxy/Deploy.php @@ -6,6 +6,9 @@ use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\StartProxy; use App\Events\ProxyStatusChanged; use App\Models\Server; +use Carbon\Carbon; +use Illuminate\Process\InvokedProcess; +use Illuminate\Support\Facades\Process; use Livewire\Component; class Deploy extends Component @@ -29,6 +32,7 @@ class Deploy extends Component 'serverRefresh' => 'proxyStatusUpdated', 'checkProxy', 'startProxy', + 'proxyChanged' => 'proxyStatusUpdated', ]; } @@ -50,7 +54,7 @@ class Deploy extends Component public function proxyStarted() { CheckProxy::run($this->server, true); - $this->dispatch('success', 'Proxy started.'); + $this->dispatch('proxyStatusUpdated'); } public function proxyStatusUpdated() @@ -61,7 +65,7 @@ class Deploy extends Component public function restart() { try { - $this->stop(); + $this->stop(forceStop: false); $this->dispatch('checkProxy'); } catch (\Throwable $e) { return handleError($e, $this); @@ -84,31 +88,53 @@ class Deploy extends Component try { $this->server->proxy->force_stop = false; $this->server->save(); - $activity = StartProxy::run($this->server); + $activity = StartProxy::run($this->server, force: true); $this->dispatch('activityMonitor', $activity->id, ProxyStatusChanged::class); } catch (\Throwable $e) { return handleError($e, $this); } } - public function stop() + public function stop(bool $forceStop = true) { try { - if ($this->server->isSwarm()) { - instant_remote_process([ - 'docker service rm coolify-proxy_traefik', - ], $this->server); - } else { - instant_remote_process([ - 'docker rm -f coolify-proxy', - ], $this->server); + $containerName = $this->server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy'; + $timeout = 30; + + $process = $this->stopContainer($containerName, $timeout); + + $startTime = Carbon::now()->getTimestamp(); + while ($process->running()) { + if (Carbon::now()->getTimestamp() - $startTime >= $timeout) { + $this->forceStopContainer($containerName); + break; + } + usleep(100000); } - $this->server->proxy->status = 'exited'; - $this->server->proxy->force_stop = true; - $this->server->save(); - $this->dispatch('proxyStatusUpdated'); + + $this->removeContainer($containerName); } catch (\Throwable $e) { return handleError($e, $this); + } finally { + $this->server->proxy->force_stop = $forceStop; + $this->server->proxy->status = 'exited'; + $this->server->save(); + $this->dispatch('proxyStatusUpdated'); } } + + private function stopContainer(string $containerName, int $timeout): InvokedProcess + { + return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); + } + + private function forceStopContainer(string $containerName) + { + instant_remote_process(["docker kill $containerName"], $this->server, throwError: false); + } + + private function removeContainer(string $containerName) + { + instant_remote_process(["docker rm -f $containerName"], $this->server, throwError: false); + } } diff --git a/app/Livewire/Server/Proxy/DynamicConfigurations.php b/app/Livewire/Server/Proxy/DynamicConfigurations.php index c858481db..6277a24bd 100644 --- a/app/Livewire/Server/Proxy/DynamicConfigurations.php +++ b/app/Livewire/Server/Proxy/DynamicConfigurations.php @@ -21,7 +21,6 @@ class DynamicConfigurations extends Component return [ "echo-private:team.{$teamId},ProxyStatusChanged" => 'loadDynamicConfigurations', 'loadDynamicConfigurations', - 'refresh' => '$refresh', ]; } @@ -42,7 +41,7 @@ class DynamicConfigurations extends Component $contents[$without_extension] = instant_remote_process(["cat {$proxy_path}/dynamic/{$file}"], $this->server); } $this->contents = $contents; - $this->dispatch('refresh'); + $this->dispatch('$refresh'); } public function mount() diff --git a/app/Livewire/Server/Proxy/Modal.php b/app/Livewire/Server/Proxy/Modal.php deleted file mode 100644 index 5679944d0..000000000 --- a/app/Livewire/Server/Proxy/Modal.php +++ /dev/null @@ -1,16 +0,0 @@ -dispatch('proxyStatusUpdated'); - } -} diff --git a/app/Livewire/Server/Proxy/NewDynamicConfiguration.php b/app/Livewire/Server/Proxy/NewDynamicConfiguration.php index e5de6eda0..2155f1e82 100644 --- a/app/Livewire/Server/Proxy/NewDynamicConfiguration.php +++ b/app/Livewire/Server/Proxy/NewDynamicConfiguration.php @@ -2,6 +2,7 @@ namespace App\Livewire\Server\Proxy; +use App\Enums\ProxyTypes; use App\Models\Server; use Livewire\Component; use Symfony\Component\Yaml\Yaml; @@ -45,7 +46,7 @@ class NewDynamicConfiguration extends Component return redirect()->route('server.index'); } $proxy_type = $this->server->proxyType(); - if ($proxy_type === 'TRAEFIK_V2') { + if ($proxy_type === ProxyTypes::TRAEFIK->value) { if (! str($this->fileName)->endsWith('.yaml') && ! str($this->fileName)->endsWith('.yml')) { $this->fileName = "{$this->fileName}.yaml"; } @@ -69,7 +70,7 @@ class NewDynamicConfiguration extends Component return; } } - if ($proxy_type === 'TRAEFIK_V2') { + if ($proxy_type === ProxyTypes::TRAEFIK->value) { $yaml = Yaml::parse($this->value); $yaml = Yaml::dump($yaml, 10, 2); $this->value = $yaml; diff --git a/app/Livewire/Server/Proxy/Show.php b/app/Livewire/Server/Proxy/Show.php index cef909a45..5ecb56a69 100644 --- a/app/Livewire/Server/Proxy/Show.php +++ b/app/Livewire/Server/Proxy/Show.php @@ -11,7 +11,7 @@ class Show extends Component public $parameters = []; - protected $listeners = ['proxyStatusUpdated']; + protected $listeners = ['proxyStatusUpdated', 'proxyChanged' => 'proxyStatusUpdated']; public function proxyStatusUpdated() { @@ -22,10 +22,7 @@ class Show extends Component { $this->parameters = get_route_parameters(); try { - $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); - if (is_null($this->server)) { - return redirect()->route('server.index'); - } + $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail(); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/Proxy/Status.php b/app/Livewire/Server/Proxy/Status.php index 8dd4dd8e6..f4f18381f 100644 --- a/app/Livewire/Server/Proxy/Status.php +++ b/app/Livewire/Server/Proxy/Status.php @@ -4,7 +4,7 @@ namespace App\Livewire\Server\Proxy; use App\Actions\Docker\GetContainersStatus; use App\Actions\Proxy\CheckProxy; -use App\Jobs\ContainerStatusJob; +use App\Actions\Proxy\StartProxy; use App\Models\Server; use Livewire\Component; @@ -16,7 +16,10 @@ class Status extends Component public int $numberOfPolls = 0; - protected $listeners = ['proxyStatusUpdated' => '$refresh', 'startProxyPolling']; + protected $listeners = [ + 'proxyStatusUpdated', + 'startProxyPolling', + ]; public function startProxyPolling() { @@ -41,11 +44,18 @@ class Status extends Component } $this->numberOfPolls++; } - CheckProxy::run($this->server, true); + $shouldStart = CheckProxy::run($this->server, true); + if ($shouldStart) { + StartProxy::run($this->server, false); + } $this->dispatch('proxyStatusUpdated'); if ($this->server->proxy->status === 'running') { $this->polling = false; $notification && $this->dispatch('success', 'Proxy is running.'); + } elseif ($this->server->proxy->status === 'exited' and ! $this->server->proxy->force_stop) { + $notification && $this->dispatch('error', 'Proxy has exited.'); + } elseif ($this->server->proxy->force_stop) { + $notification && $this->dispatch('error', 'Proxy is stopped manually.'); } else { $notification && $this->dispatch('error', 'Proxy is not running.'); } diff --git a/app/Livewire/Server/Resources.php b/app/Livewire/Server/Resources.php index 800344ac3..f549b43cb 100644 --- a/app/Livewire/Server/Resources.php +++ b/app/Livewire/Server/Resources.php @@ -15,7 +15,9 @@ class Resources extends Component public $parameters = []; - public Collection $unmanagedContainers; + public Collection $containers; + + public $activeTab = 'managed'; public function getListeners() { @@ -50,14 +52,29 @@ class Resources extends Component public function refreshStatus() { $this->server->refresh(); - $this->loadUnmanagedContainers(); + if ($this->activeTab === 'managed') { + $this->loadManagedContainers(); + } else { + $this->loadUnmanagedContainers(); + } $this->dispatch('success', 'Resource statuses refreshed.'); } + public function loadManagedContainers() + { + try { + $this->activeTab = 'managed'; + $this->containers = $this->server->refresh()->definedResources(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function loadUnmanagedContainers() { + $this->activeTab = 'unmanaged'; try { - $this->unmanagedContainers = $this->server->loadUnmanagedContainers(); + $this->containers = $this->server->loadUnmanagedContainers(); } catch (\Throwable $e) { return handleError($e, $this); } @@ -65,13 +82,14 @@ class Resources extends Component public function mount() { - $this->unmanagedContainers = collect(); + $this->containers = collect(); $this->parameters = get_route_parameters(); try { $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); if (is_null($this->server)) { return redirect()->route('server.index'); } + $this->loadManagedContainers(); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index 7ebf90115..bb9188f1c 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -2,36 +2,247 @@ namespace App\Livewire\Server; +use App\Actions\Server\StartSentinel; +use App\Actions\Server\StopSentinel; use App\Models\Server; -use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class Show extends Component { - use AuthorizesRequests; + public Server $server; - public ?Server $server = null; + #[Validate(['required'])] + public string $name; - public $parameters = []; + #[Validate(['nullable'])] + public ?string $description = null; - protected $listeners = ['serverInstalled' => '$refresh']; + #[Validate(['required'])] + public string $ip; - public function mount() + #[Validate(['required'])] + public string $user; + + #[Validate(['required'])] + public string $port; + + #[Validate(['nullable'])] + public ?string $validationLogs = null; + + #[Validate(['nullable', 'url'])] + public ?string $wildcardDomain = null; + + #[Validate(['required'])] + public bool $isReachable; + + #[Validate(['required'])] + public bool $isUsable; + + #[Validate(['required'])] + public bool $isSwarmManager; + + #[Validate(['required'])] + public bool $isSwarmWorker; + + #[Validate(['required'])] + public bool $isBuildServer; + + #[Validate(['required'])] + public bool $isMetricsEnabled; + + #[Validate(['required'])] + public string $sentinelToken; + + #[Validate(['nullable'])] + public ?string $sentinelUpdatedAt = null; + + #[Validate(['required', 'integer', 'min:1'])] + public int $sentinelMetricsRefreshRateSeconds; + + #[Validate(['required', 'integer', 'min:1'])] + public int $sentinelMetricsHistoryDays; + + #[Validate(['required', 'integer', 'min:10'])] + public int $sentinelPushIntervalSeconds; + + #[Validate(['nullable', 'url'])] + public ?string $sentinelCustomUrl = null; + + #[Validate(['required'])] + public bool $isSentinelEnabled; + + #[Validate(['required'])] + public bool $isSentinelDebugEnabled; + + #[Validate(['required'])] + public string $serverTimezone; + + #[Locked] + public array $timezones; + + public function getListeners() + { + $teamId = auth()->user()->currentTeam()->id; + + return [ + "echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'refresh', + 'refreshServerShow' => 'refresh', + ]; + } + + public function mount(string $server_uuid) { - $this->parameters = get_route_parameters(); try { - $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); - if (is_null($this->server)) { - return redirect()->route('server.index'); - } + $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray(); + $this->syncData(); } catch (\Throwable $e) { return handleError($e, $this); } } + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->server->name = $this->name; + $this->server->description = $this->description; + $this->server->ip = $this->ip; + $this->server->user = $this->user; + $this->server->port = $this->port; + $this->server->validation_logs = $this->validationLogs; + $this->server->save(); + + $this->server->settings->is_swarm_manager = $this->isSwarmManager; + $this->server->settings->wildcard_domain = $this->wildcardDomain; + $this->server->settings->is_swarm_worker = $this->isSwarmWorker; + $this->server->settings->is_build_server = $this->isBuildServer; + $this->server->settings->is_metrics_enabled = $this->isMetricsEnabled; + $this->server->settings->sentinel_token = $this->sentinelToken; + $this->server->settings->sentinel_metrics_refresh_rate_seconds = $this->sentinelMetricsRefreshRateSeconds; + $this->server->settings->sentinel_metrics_history_days = $this->sentinelMetricsHistoryDays; + $this->server->settings->sentinel_push_interval_seconds = $this->sentinelPushIntervalSeconds; + $this->server->settings->sentinel_custom_url = $this->sentinelCustomUrl; + $this->server->settings->is_sentinel_enabled = $this->isSentinelEnabled; + $this->server->settings->is_sentinel_debug_enabled = $this->isSentinelDebugEnabled; + $this->server->settings->server_timezone = $this->serverTimezone; + $this->server->settings->save(); + } else { + $this->name = $this->server->name; + $this->description = $this->server->description; + $this->ip = $this->server->ip; + $this->user = $this->server->user; + $this->port = $this->server->port; + + $this->wildcardDomain = $this->server->settings->wildcard_domain; + $this->isReachable = $this->server->settings->is_reachable; + $this->isUsable = $this->server->settings->is_usable; + $this->isSwarmManager = $this->server->settings->is_swarm_manager; + $this->isSwarmWorker = $this->server->settings->is_swarm_worker; + $this->isBuildServer = $this->server->settings->is_build_server; + $this->isMetricsEnabled = $this->server->settings->is_metrics_enabled; + $this->sentinelToken = $this->server->settings->sentinel_token; + $this->sentinelMetricsRefreshRateSeconds = $this->server->settings->sentinel_metrics_refresh_rate_seconds; + $this->sentinelMetricsHistoryDays = $this->server->settings->sentinel_metrics_history_days; + $this->sentinelPushIntervalSeconds = $this->server->settings->sentinel_push_interval_seconds; + $this->sentinelCustomUrl = $this->server->settings->sentinel_custom_url; + $this->isSentinelEnabled = $this->server->settings->is_sentinel_enabled; + $this->isSentinelDebugEnabled = $this->server->settings->is_sentinel_debug_enabled; + $this->sentinelUpdatedAt = $this->server->settings->updated_at; + $this->serverTimezone = $this->server->settings->server_timezone; + } + } + + public function refresh() + { + $this->syncData(); + $this->dispatch('$refresh'); + } + + public function validateServer($install = true) + { + try { + $this->validationLogs = $this->server->validation_logs = null; + $this->server->save(); + $this->dispatch('init', $install); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function checkLocalhostConnection() + { + $this->syncData(true); + ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(); + if ($uptime) { + $this->dispatch('success', 'Server is reachable.'); + $this->server->settings->is_reachable = $this->isReachable = true; + $this->server->settings->is_usable = $this->isUsable = true; + $this->server->settings->save(); + $this->dispatch('proxyStatusUpdated'); + } else { + $this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.

Check this documentation for further help.

Error: '.$error); + + return; + } + } + + public function restartSentinel() + { + $this->server->restartSentinel(); + $this->dispatch('success', 'Sentinel restarted.'); + } + + public function updatedIsSentinelDebugEnabled($value) + { + $this->submit(); + $this->restartSentinel(); + } + + public function updatedIsMetricsEnabled($value) + { + $this->submit(); + $this->restartSentinel(); + } + + public function updatedIsSentinelEnabled($value) + { + if ($value === true) { + StartSentinel::run($this->server, true); + } else { + $this->isMetricsEnabled = false; + $this->isSentinelDebugEnabled = false; + StopSentinel::dispatch($this->server); + } + $this->submit(); + + } + + public function regenerateSentinelToken() + { + try { + $this->server->settings->generateSentinelToken(); + $this->dispatch('success', 'Token regenerated & Sentinel restarted.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function instantSave() + { + $this->submit(); + } + public function submit() { - $this->dispatch('serverRefresh', false); + try { + $this->syncData(true); + $this->dispatch('success', 'Server updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function render() diff --git a/app/Livewire/Server/ShowPrivateKey.php b/app/Livewire/Server/ShowPrivateKey.php deleted file mode 100644 index 578a08967..000000000 --- a/app/Livewire/Server/ShowPrivateKey.php +++ /dev/null @@ -1,59 +0,0 @@ -server->private_key_id; - refresh_server_connection($this->server->privateKey); - $this->server->update([ - 'private_key_id' => $newPrivateKeyId, - ]); - $this->server->refresh(); - refresh_server_connection($this->server->privateKey); - $this->checkConnection(); - } catch (\Throwable $e) { - $this->server->update([ - 'private_key_id' => $oldPrivateKeyId, - ]); - $this->server->refresh(); - refresh_server_connection($this->server->privateKey); - - return handleError($e, $this); - } - } - - public function checkConnection() - { - try { - ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(); - if ($uptime) { - $this->dispatch('success', 'Server is reachable.'); - } else { - ray($error); - $this->dispatch('error', 'Server is not reachable.
Please validate your configuration and connection.

Check this documentation for further help.'); - - return; - } - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function mount() - { - $this->parameters = get_route_parameters(); - } -} diff --git a/app/Livewire/Server/ValidateAndInstall.php b/app/Livewire/Server/ValidateAndInstall.php index bd33937e0..8c5bc23ed 100644 --- a/app/Livewire/Server/ValidateAndInstall.php +++ b/app/Livewire/Server/ValidateAndInstall.php @@ -87,7 +87,10 @@ class ValidateAndInstall extends Component { ['uptime' => $this->uptime, 'error' => $error] = $this->server->validateConnection(); if (! $this->uptime) { - $this->error = 'Server is not reachable. Please validate your configuration and connection.

Check this documentation for further help.

Error: '.$error; + $this->error = 'Server is not reachable. Please validate your configuration and connection.
Check this documentation for further help.

Error: '.$error.'
'; + $this->server->update([ + 'validation_logs' => $this->error, + ]); return; } @@ -99,6 +102,9 @@ class ValidateAndInstall extends Component $this->supported_os_type = $this->server->validateOS(); if (! $this->supported_os_type) { $this->error = 'Server OS type is not supported. Please install Docker manually before continuing: documentation.'; + $this->server->update([ + 'validation_logs' => $this->error, + ]); return; } @@ -113,6 +119,9 @@ class ValidateAndInstall extends Component if ($this->install) { if ($this->number_of_tries == $this->max_tries) { $this->error = 'Docker Engine could not be installed. Please install Docker manually before continuing: documentation.'; + $this->server->update([ + 'validation_logs' => $this->error, + ]); return; } else { @@ -126,6 +135,9 @@ class ValidateAndInstall extends Component } } else { $this->error = 'Docker Engine is not installed. Please install Docker manually before continuing: documentation.'; + $this->server->update([ + 'validation_logs' => $this->error, + ]); return; } @@ -143,10 +155,14 @@ class ValidateAndInstall extends Component } else { $this->docker_version = $this->server->validateDockerEngineVersion(); if ($this->docker_version) { - $this->dispatch('serverInstalled'); + $this->dispatch('refreshServerShow'); + $this->dispatch('refreshBoardingIndex'); $this->dispatch('success', 'Server validated.'); } else { $this->error = 'Docker Engine version is not 22+. Please install Docker manually before continuing: documentation.'; + $this->server->update([ + 'validation_logs' => $this->error, + ]); return; } diff --git a/app/Livewire/Settings/Backup.php b/app/Livewire/Settings/Backup.php deleted file mode 100644 index 08ad04b2d..000000000 --- a/app/Livewire/Settings/Backup.php +++ /dev/null @@ -1,97 +0,0 @@ - 'required', - 'database.name' => 'required', - 'database.description' => 'nullable', - 'database.postgres_user' => 'required', - 'database.postgres_password' => 'required', - - ]; - - protected $validationAttributes = [ - 'database.uuid' => 'uuid', - 'database.name' => 'name', - 'database.description' => 'description', - 'database.postgres_user' => 'postgres user', - 'database.postgres_password' => 'postgres password', - ]; - - public function mount() - { - $this->backup = $this->database?->scheduledBackups->first() ?? null; - $this->executions = $this->backup?->executions ?? []; - } - - public function add_coolify_database() - { - try { - $server = Server::findOrFail(0); - $out = instant_remote_process(['docker inspect coolify-db'], $server); - $envs = format_docker_envs_to_json($out); - $postgres_password = $envs['POSTGRES_PASSWORD']; - $postgres_user = $envs['POSTGRES_USER']; - $postgres_db = $envs['POSTGRES_DB']; - $this->database = StandalonePostgresql::create([ - 'id' => 0, - 'name' => 'coolify-db', - 'description' => 'Coolify database', - 'postgres_user' => $postgres_user, - 'postgres_password' => $postgres_password, - 'postgres_db' => $postgres_db, - 'status' => 'running', - 'destination_type' => 'App\Models\StandaloneDocker', - 'destination_id' => 0, - ]); - $this->backup = ScheduledDatabaseBackup::create([ - 'id' => 0, - 'enabled' => true, - 'save_s3' => false, - 'frequency' => '0 0 * * *', - 'database_id' => $this->database->id, - 'database_type' => 'App\Models\StandalonePostgresql', - 'team_id' => currentTeam()->id, - ]); - $this->database->refresh(); - $this->backup->refresh(); - $this->s3s = S3Storage::whereTeamId(0)->get(); - } catch (\Exception $e) { - return handleError($e, $this); - } - } - - public function backup_now() - { - dispatch(new DatabaseBackupJob( - backup: $this->backup - )); - $this->dispatch('success', 'Backup queued. It will be available in a few minutes.'); - } - - public function submit() - { - $this->dispatch('success', 'Backup updated.'); - } -} diff --git a/app/Livewire/Settings/Configuration.php b/app/Livewire/Settings/Configuration.php deleted file mode 100644 index 4dfa16e30..000000000 --- a/app/Livewire/Settings/Configuration.php +++ /dev/null @@ -1,105 +0,0 @@ - 'nullable', - 'settings.resale_license' => 'nullable', - 'settings.public_port_min' => 'required', - 'settings.public_port_max' => 'required', - 'settings.custom_dns_servers' => 'nullable', - ]; - - protected $validationAttributes = [ - 'settings.fqdn' => 'FQDN', - 'settings.resale_license' => 'Resale License', - 'settings.public_port_min' => 'Public port min', - 'settings.public_port_max' => 'Public port max', - 'settings.custom_dns_servers' => 'Custom DNS servers', - ]; - - public function mount() - { - $this->do_not_track = $this->settings->do_not_track; - $this->is_auto_update_enabled = $this->settings->is_auto_update_enabled; - $this->is_registration_enabled = $this->settings->is_registration_enabled; - // $this->next_channel = $this->settings->next_channel; - $this->is_dns_validation_enabled = $this->settings->is_dns_validation_enabled; - } - - public function instantSave() - { - $this->settings->do_not_track = $this->do_not_track; - $this->settings->is_auto_update_enabled = $this->is_auto_update_enabled; - $this->settings->is_registration_enabled = $this->is_registration_enabled; - $this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled; - // if ($this->next_channel) { - // $this->settings->next_channel = false; - // $this->next_channel = false; - // } else { - // $this->settings->next_channel = $this->next_channel; - // } - $this->settings->save(); - $this->dispatch('success', 'Settings updated!'); - } - - public function submit() - { - try { - $error_show = false; - $this->server = Server::findOrFail(0); - $this->resetErrorBag(); - if ($this->settings->public_port_min > $this->settings->public_port_max) { - $this->addError('settings.public_port_min', 'The minimum port must be lower than the maximum port.'); - - return; - } - $this->validate(); - - if ($this->settings->is_dns_validation_enabled && $this->settings->fqdn) { - if (! validate_dns_entry($this->settings->fqdn, $this->server)) { - $this->dispatch('error', "Validating DNS failed.

Make sure you have added the DNS records correctly.

{$this->settings->fqdn}->{$this->server->ip}

Check this documentation for further help."); - $error_show = true; - } - } - if ($this->settings->fqdn) { - check_domain_usage(domain: $this->settings->fqdn); - } - $this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->replaceEnd(',', '')->trim(); - $this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->trim()->explode(',')->map(function ($dns) { - return str($dns)->trim()->lower(); - }); - $this->settings->custom_dns_servers = $this->settings->custom_dns_servers->unique(); - $this->settings->custom_dns_servers = $this->settings->custom_dns_servers->implode(','); - - $this->settings->save(); - $this->server->setupDynamicProxyConfiguration(); - if (! $error_show) { - $this->dispatch('success', 'Instance settings updated successfully!'); - } - } catch (\Exception $e) { - return handleError($e, $this); - } - } -} diff --git a/app/Livewire/Settings/Email.php b/app/Livewire/Settings/Email.php deleted file mode 100644 index bd7f8201e..000000000 --- a/app/Livewire/Settings/Email.php +++ /dev/null @@ -1,127 +0,0 @@ - 'nullable|boolean', - 'settings.smtp_host' => 'required', - 'settings.smtp_port' => 'required|numeric', - 'settings.smtp_encryption' => 'nullable', - 'settings.smtp_username' => 'nullable', - 'settings.smtp_password' => 'nullable', - 'settings.smtp_timeout' => 'nullable', - 'settings.smtp_from_address' => 'required|email', - 'settings.smtp_from_name' => 'required', - 'settings.resend_enabled' => 'nullable|boolean', - 'settings.resend_api_key' => 'nullable', - - ]; - - protected $validationAttributes = [ - 'settings.smtp_from_address' => 'From Address', - 'settings.smtp_from_name' => 'From Name', - 'settings.smtp_recipients' => 'Recipients', - 'settings.smtp_host' => 'Host', - 'settings.smtp_port' => 'Port', - 'settings.smtp_encryption' => 'Encryption', - 'settings.smtp_username' => 'Username', - 'settings.smtp_password' => 'Password', - 'settings.smtp_timeout' => 'Timeout', - 'settings.resend_api_key' => 'Resend API Key', - ]; - - public function mount() - { - $this->emails = auth()->user()->email; - } - - public function submitFromFields() - { - try { - $this->resetErrorBag(); - $this->validate([ - 'settings.smtp_from_address' => 'required|email', - 'settings.smtp_from_name' => 'required', - ]); - $this->settings->save(); - $this->dispatch('success', 'Settings saved.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function submitResend() - { - try { - $this->resetErrorBag(); - $this->validate([ - 'settings.smtp_from_address' => 'required|email', - 'settings.smtp_from_name' => 'required', - 'settings.resend_api_key' => 'required', - ]); - $this->settings->save(); - $this->dispatch('success', 'Settings saved.'); - } catch (\Throwable $e) { - $this->settings->resend_enabled = false; - - return handleError($e, $this); - } - } - - public function instantSaveResend() - { - try { - $this->settings->smtp_enabled = false; - $this->submitResend(); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function instantSave() - { - try { - $this->settings->resend_enabled = false; - $this->submit(); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function submit() - { - try { - $this->resetErrorBag(); - $this->validate([ - 'settings.smtp_from_address' => 'required|email', - 'settings.smtp_from_name' => 'required', - 'settings.smtp_host' => 'required', - 'settings.smtp_port' => 'required|numeric', - 'settings.smtp_encryption' => 'nullable', - 'settings.smtp_username' => 'nullable', - 'settings.smtp_password' => 'nullable', - 'settings.smtp_timeout' => 'nullable', - ]); - $this->settings->save(); - $this->dispatch('success', 'Settings saved.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function sendTestNotification() - { - $this->settings?->notify(new Test($this->emails)); - $this->dispatch('success', 'Test email sent.'); - } -} diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php index f6f918933..2991b8ae8 100644 --- a/app/Livewire/Settings/Index.php +++ b/app/Livewire/Settings/Index.php @@ -2,41 +2,225 @@ namespace App\Livewire\Settings; +use App\Jobs\CheckForUpdatesJob; use App\Models\InstanceSettings; -use App\Models\S3Storage; -use App\Models\StandalonePostgresql; +use App\Models\Server; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class Index extends Component { public InstanceSettings $settings; - public StandalonePostgresql $database; + protected Server $server; - public $s3s; + #[Locked] + public $timezones; - public function mount() - { - if (isInstanceAdmin()) { - $settings = InstanceSettings::get(); - $database = StandalonePostgresql::whereName('coolify-db')->first(); - $s3s = S3Storage::whereTeamId(0)->get() ?? []; - if ($database) { - if ($database->status !== 'running') { - $database->status = 'running'; - $database->save(); - } - $this->database = $database; - } - $this->settings = $settings; - $this->s3s = $s3s; - } else { - return redirect()->route('dashboard'); - } - } + #[Validate('boolean')] + public bool $is_auto_update_enabled; + + #[Validate('nullable|string|max:255')] + public ?string $fqdn = null; + + #[Validate('nullable|string|max:255')] + public ?string $resale_license = null; + + #[Validate('required|integer|min:1025|max:65535')] + public int $public_port_min; + + #[Validate('required|integer|min:1025|max:65535')] + public int $public_port_max; + + #[Validate('nullable|string')] + public ?string $custom_dns_servers = null; + + #[Validate('nullable|string|max:255')] + public ?string $instance_name = null; + + #[Validate('nullable|string')] + public ?string $allowed_ips = null; + + #[Validate('nullable|string')] + public ?string $public_ipv4 = null; + + #[Validate('nullable|string')] + public ?string $public_ipv6 = null; + + #[Validate('string')] + public string $auto_update_frequency; + + #[Validate('string')] + public string $update_check_frequency; + + #[Validate('required|string|timezone')] + public string $instance_timezone; + + #[Validate('boolean')] + public bool $do_not_track; + + #[Validate('boolean')] + public bool $is_registration_enabled; + + #[Validate('boolean')] + public bool $is_dns_validation_enabled; + + #[Validate('boolean')] + public bool $is_api_enabled; + + #[Validate('boolean')] + public bool $disable_two_step_confirmation; public function render() { return view('livewire.settings.index'); } + + public function mount() + { + if (! isInstanceAdmin()) { + return redirect()->route('dashboard'); + } else { + $this->settings = instanceSettings(); + $this->fqdn = $this->settings->fqdn; + $this->resale_license = $this->settings->resale_license; + $this->public_port_min = $this->settings->public_port_min; + $this->public_port_max = $this->settings->public_port_max; + $this->custom_dns_servers = $this->settings->custom_dns_servers; + $this->instance_name = $this->settings->instance_name; + $this->allowed_ips = $this->settings->allowed_ips; + $this->public_ipv4 = $this->settings->public_ipv4; + $this->public_ipv6 = $this->settings->public_ipv6; + $this->do_not_track = $this->settings->do_not_track; + $this->is_auto_update_enabled = $this->settings->is_auto_update_enabled; + $this->is_registration_enabled = $this->settings->is_registration_enabled; + $this->is_dns_validation_enabled = $this->settings->is_dns_validation_enabled; + $this->is_api_enabled = $this->settings->is_api_enabled; + $this->auto_update_frequency = $this->settings->auto_update_frequency; + $this->update_check_frequency = $this->settings->update_check_frequency; + $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray(); + $this->instance_timezone = $this->settings->instance_timezone; + $this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation; + } + } + + public function instantSave($isSave = true) + { + $this->settings->fqdn = $this->fqdn; + $this->settings->resale_license = $this->resale_license; + $this->settings->public_port_min = $this->public_port_min; + $this->settings->public_port_max = $this->public_port_max; + $this->settings->custom_dns_servers = $this->custom_dns_servers; + $this->settings->instance_name = $this->instance_name; + $this->settings->allowed_ips = $this->allowed_ips; + $this->settings->public_ipv4 = $this->public_ipv4; + $this->settings->public_ipv6 = $this->public_ipv6; + $this->settings->do_not_track = $this->do_not_track; + $this->settings->is_auto_update_enabled = $this->is_auto_update_enabled; + $this->settings->is_registration_enabled = $this->is_registration_enabled; + $this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled; + $this->settings->is_api_enabled = $this->is_api_enabled; + $this->settings->auto_update_frequency = $this->auto_update_frequency; + $this->settings->update_check_frequency = $this->update_check_frequency; + $this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation; + $this->settings->instance_timezone = $this->instance_timezone; + if ($isSave) { + $this->settings->save(); + $this->dispatch('success', 'Settings updated!'); + } + } + + public function submit() + { + try { + $error_show = false; + $this->server = Server::findOrFail(0); + $this->resetErrorBag(); + if ($this->settings->public_port_min > $this->settings->public_port_max) { + $this->addError('settings.public_port_min', 'The minimum port must be lower than the maximum port.'); + + return; + } + $this->validate(); + + if ($this->is_auto_update_enabled && ! validate_cron_expression($this->auto_update_frequency)) { + $this->dispatch('error', 'Invalid Cron / Human expression for Auto Update Frequency.'); + if (empty($this->auto_update_frequency)) { + $this->auto_update_frequency = '0 0 * * *'; + } + + return; + } + + if (! validate_cron_expression($this->update_check_frequency)) { + $this->dispatch('error', 'Invalid Cron / Human expression for Update Check Frequency.'); + if (empty($this->update_check_frequency)) { + $this->update_check_frequency = '0 * * * *'; + } + + return; + } + + if ($this->settings->is_dns_validation_enabled && $this->settings->fqdn) { + if (! validate_dns_entry($this->settings->fqdn, $this->server)) { + $this->dispatch('error', "Validating DNS failed.

Make sure you have added the DNS records correctly.

{$this->settings->fqdn}->{$this->server->ip}

Check this documentation for further help."); + $error_show = true; + } + } + if ($this->settings->fqdn) { + check_domain_usage(domain: $this->settings->fqdn); + } + $this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->replaceEnd(',', '')->trim(); + $this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->trim()->explode(',')->map(function ($dns) { + return str($dns)->trim()->lower(); + }); + $this->settings->custom_dns_servers = $this->settings->custom_dns_servers->unique(); + $this->settings->custom_dns_servers = $this->settings->custom_dns_servers->implode(','); + + $this->settings->allowed_ips = str($this->settings->allowed_ips)->replaceEnd(',', '')->trim(); + $this->settings->allowed_ips = str($this->settings->allowed_ips)->trim()->explode(',')->map(function ($ip) { + return str($ip)->trim(); + }); + $this->settings->allowed_ips = $this->settings->allowed_ips->unique(); + $this->settings->allowed_ips = $this->settings->allowed_ips->implode(','); + + $this->instantSave(isSave: false); + + $this->settings->save(); + $this->server->setupDynamicProxyConfiguration(); + if (! $error_show) { + $this->dispatch('success', 'Instance settings updated successfully!'); + } + } catch (\Exception $e) { + return handleError($e, $this); + } + } + + public function checkManually() + { + CheckForUpdatesJob::dispatchSync(); + $this->dispatch('updateAvailable'); + $settings = instanceSettings(); + if ($settings->new_version_available) { + $this->dispatch('success', 'New version available!'); + } else { + $this->dispatch('success', 'No new version available.'); + } + } + + public function toggleTwoStepConfirmation($password) + { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); + + return; + } + + $this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation = true; + $this->settings->save(); + $this->dispatch('success', 'Two step confirmation has been disabled.'); + } } diff --git a/app/Livewire/Settings/License.php b/app/Livewire/Settings/License.php index 212bc95be..79f8269f3 100644 --- a/app/Livewire/Settings/License.php +++ b/app/Livewire/Settings/License.php @@ -28,8 +28,11 @@ class License extends Component if (! isCloud()) { abort(404); } + if (! isInstanceAdmin()) { + return redirect()->route('home'); + } $this->instance_id = config('app.id'); - $this->settings = InstanceSettings::get(); + $this->settings = instanceSettings(); } public function render() @@ -47,7 +50,6 @@ class License extends Component $this->dispatch('reloadWindow'); } catch (\Throwable $e) { session()->flash('error', 'Something went wrong. Please contact support.
Error: '.$e->getMessage()); - ray($e->getMessage()); return redirect()->route('settings.license'); } diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php new file mode 100644 index 000000000..1b0599ffe --- /dev/null +++ b/app/Livewire/SettingsBackup.php @@ -0,0 +1,125 @@ +route('dashboard'); + } else { + $settings = instanceSettings(); + $this->database = StandalonePostgresql::whereName('coolify-db')->first(); + $s3s = S3Storage::whereTeamId(0)->get() ?? []; + if ($this->database) { + $this->uuid = $this->database->uuid; + $this->name = $this->database->name; + $this->description = $this->database->description; + $this->postgres_user = $this->database->postgres_user; + $this->postgres_password = $this->database->postgres_password; + + if ($this->database->status !== 'running') { + $this->database->status = 'running'; + $this->database->save(); + } + $this->backup = $this->database->scheduledBackups->first(); + $this->executions = $this->backup->executions; + } + $this->settings = $settings; + $this->s3s = $s3s; + } + } + + public function addCoolifyDatabase() + { + try { + $server = Server::findOrFail(0); + $out = instant_remote_process(['docker inspect coolify-db'], $server); + $envs = format_docker_envs_to_json($out); + $postgres_password = $envs['POSTGRES_PASSWORD']; + $postgres_user = $envs['POSTGRES_USER']; + $postgres_db = $envs['POSTGRES_DB']; + $this->database = StandalonePostgresql::create([ + 'id' => 0, + 'name' => 'coolify-db', + 'description' => 'Coolify database', + 'postgres_user' => $postgres_user, + 'postgres_password' => $postgres_password, + 'postgres_db' => $postgres_db, + 'status' => 'running', + 'destination_type' => \App\Models\StandaloneDocker::class, + 'destination_id' => 0, + ]); + $this->backup = ScheduledDatabaseBackup::create([ + 'id' => 0, + 'enabled' => true, + 'save_s3' => false, + 'frequency' => '0 0 * * *', + 'database_id' => $this->database->id, + 'database_type' => \App\Models\StandalonePostgresql::class, + 'team_id' => currentTeam()->id, + ]); + $this->database->refresh(); + $this->backup->refresh(); + $this->s3s = S3Storage::whereTeamId(0)->get(); + + $this->uuid = $this->database->uuid; + $this->name = $this->database->name; + $this->description = $this->database->description; + $this->postgres_user = $this->database->postgres_user; + $this->postgres_password = $this->database->postgres_password; + $this->executions = $this->backup->executions; + + } catch (\Exception $e) { + return handleError($e, $this); + } + } + + public function submit() + { + $this->database->update([ + 'name' => $this->name, + 'description' => $this->description, + 'postgres_user' => $this->postgres_user, + 'postgres_password' => $this->postgres_password, + ]); + $this->dispatch('success', 'Backup updated.'); + } +} diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php new file mode 100644 index 000000000..61f720b3a --- /dev/null +++ b/app/Livewire/SettingsEmail.php @@ -0,0 +1,117 @@ +route('dashboard'); + } + $this->settings = instanceSettings(); + $this->syncData(); + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->settings->smtp_enabled = $this->smtpEnabled; + $this->settings->smtp_host = $this->smtpHost; + $this->settings->smtp_port = $this->smtpPort; + $this->settings->smtp_encryption = $this->smtpEncryption; + $this->settings->smtp_username = $this->smtpUsername; + $this->settings->smtp_password = $this->smtpPassword; + $this->settings->smtp_timeout = $this->smtpTimeout; + $this->settings->smtp_from_address = $this->smtpFromAddress; + $this->settings->smtp_from_name = $this->smtpFromName; + + $this->settings->resend_enabled = $this->resendEnabled; + $this->settings->resend_api_key = $this->resendApiKey; + $this->settings->save(); + } else { + $this->smtpEnabled = $this->settings->smtp_enabled; + $this->smtpHost = $this->settings->smtp_host; + $this->smtpPort = $this->settings->smtp_port; + $this->smtpEncryption = $this->settings->smtp_encryption; + $this->smtpUsername = $this->settings->smtp_username; + $this->smtpPassword = $this->settings->smtp_password; + $this->smtpTimeout = $this->settings->smtp_timeout; + $this->smtpFromAddress = $this->settings->smtp_from_address; + $this->smtpFromName = $this->settings->smtp_from_name; + + $this->resendEnabled = $this->settings->resend_enabled; + $this->resendApiKey = $this->settings->resend_api_key; + } + } + + public function submit() + { + try { + $this->resetErrorBag(); + $this->syncData(true); + $this->dispatch('success', 'Settings saved.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function instantSave(string $type) + { + try { + if ($type === 'SMTP') { + $this->resendEnabled = false; + } else { + $this->smtpEnabled = false; + } + $this->syncData(true); + if ($this->smtpEnabled || $this->resendEnabled) { + $this->dispatch('success', "{$type} enabled."); + } else { + $this->dispatch('success', "{$type} disabled."); + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } +} diff --git a/app/Livewire/Settings/Auth.php b/app/Livewire/SettingsOauth.php similarity index 89% rename from app/Livewire/Settings/Auth.php rename to app/Livewire/SettingsOauth.php index 783b163e0..17b3b89a3 100644 --- a/app/Livewire/Settings/Auth.php +++ b/app/Livewire/SettingsOauth.php @@ -1,11 +1,11 @@ route('home'); + } $this->oauth_settings_map = OauthSetting::all()->sortBy('provider')->reduce(function ($carry, $setting) { $carry[$setting->provider] = $setting; diff --git a/app/Livewire/SharedVariables/Environment/Show.php b/app/Livewire/SharedVariables/Environment/Show.php index e025d8f7c..daf1df212 100644 --- a/app/Livewire/SharedVariables/Environment/Show.php +++ b/app/Livewire/SharedVariables/Environment/Show.php @@ -16,7 +16,7 @@ class Show extends Component public array $parameters; - protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey' => 'saveKey']; + protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey']; public function saveKey($data) { diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index ee28f8847..07cef54f9 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -4,8 +4,6 @@ namespace App\Livewire\Source\Github; use App\Jobs\GithubAppPermissionJob; use App\Models\GithubApp; -use App\Models\InstanceSettings; -use Illuminate\Support\Facades\Http; use Livewire\Component; class Change extends Component @@ -94,51 +92,53 @@ class Change extends Component // } public function mount() { - $github_app_uuid = request()->github_app_uuid; - $this->github_app = GithubApp::where('uuid', $github_app_uuid)->first(); - if (! $this->github_app) { - return redirect()->route('source.all'); - } - $this->applications = $this->github_app->applications; - $settings = InstanceSettings::get(); - $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); + try { + $github_app_uuid = request()->github_app_uuid; + $this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail(); - $this->name = str($this->github_app->name)->kebab(); - $this->fqdn = $settings->fqdn; + $this->applications = $this->github_app->applications; + $settings = instanceSettings(); + $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); - if ($settings->public_ipv4) { - $this->ipv4 = 'http://'.$settings->public_ipv4.':'.config('app.port'); - } - if ($settings->public_ipv6) { - $this->ipv6 = 'http://'.$settings->public_ipv6.':'.config('app.port'); - } - if ($this->github_app->installation_id && session('from')) { - $source_id = data_get(session('from'), 'source_id'); - if (! $source_id || $this->github_app->id !== $source_id) { - session()->forget('from'); - } else { - $parameters = data_get(session('from'), 'parameters'); - $back = data_get(session('from'), 'back'); - $environment_name = data_get($parameters, 'environment_name'); - $project_uuid = data_get($parameters, 'project_uuid'); - $type = data_get($parameters, 'type'); - $destination = data_get($parameters, 'destination'); - session()->forget('from'); + $this->name = str($this->github_app->name)->kebab(); + $this->fqdn = $settings->fqdn; - return redirect()->route($back, [ - 'environment_name' => $environment_name, - 'project_uuid' => $project_uuid, - 'type' => $type, - 'destination' => $destination, - ]); + if ($settings->public_ipv4) { + $this->ipv4 = 'http://'.$settings->public_ipv4.':'.config('app.port'); } - } - $this->parameters = get_route_parameters(); - if (isCloud() && ! isDev()) { - $this->webhook_endpoint = config('app.url'); - } else { - $this->webhook_endpoint = $this->ipv4; - $this->is_system_wide = $this->github_app->is_system_wide; + if ($settings->public_ipv6) { + $this->ipv6 = 'http://'.$settings->public_ipv6.':'.config('app.port'); + } + if ($this->github_app->installation_id && session('from')) { + $source_id = data_get(session('from'), 'source_id'); + if (! $source_id || $this->github_app->id !== $source_id) { + session()->forget('from'); + } else { + $parameters = data_get(session('from'), 'parameters'); + $back = data_get(session('from'), 'back'); + $environment_name = data_get($parameters, 'environment_name'); + $project_uuid = data_get($parameters, 'project_uuid'); + $type = data_get($parameters, 'type'); + $destination = data_get($parameters, 'destination'); + session()->forget('from'); + + return redirect()->route($back, [ + 'environment_name' => $environment_name, + 'project_uuid' => $project_uuid, + 'type' => $type, + 'destination' => $destination, + ]); + } + } + $this->parameters = get_route_parameters(); + if (isCloud() && ! isDev()) { + $this->webhook_endpoint = config('app.url'); + } else { + $this->webhook_endpoint = $this->ipv4; + $this->is_system_wide = $this->github_app->is_system_wide; + } + } catch (\Throwable $e) { + return handleError($e, $this); } } diff --git a/app/Livewire/Source/Github/Create.php b/app/Livewire/Source/Github/Create.php index f85e8646e..136d3525e 100644 --- a/app/Livewire/Source/Github/Create.php +++ b/app/Livewire/Source/Github/Create.php @@ -23,7 +23,7 @@ class Create extends Component public function mount() { - $this->name = generate_random_name(); + $this->name = substr(generate_random_name(), 0, 30); } public function createGitHubApp() diff --git a/app/Livewire/Storage/Create.php b/app/Livewire/Storage/Create.php index 1ccc3997c..c5250e1e3 100644 --- a/app/Livewire/Storage/Create.php +++ b/app/Livewire/Storage/Create.php @@ -43,15 +43,17 @@ class Create extends Component 'endpoint' => 'Endpoint', ]; - public function mount() + public function updatedEndpoint($value) { - if (isDev()) { - $this->name = 'Local MinIO'; - $this->description = 'Local MinIO'; - $this->key = 'minioadmin'; - $this->secret = 'minioadmin'; - $this->bucket = 'local'; - $this->endpoint = 'http://coolify-minio:9000'; + if (! str($value)->startsWith('https://') && ! str($value)->startsWith('http://')) { + $this->endpoint = 'https://'.$value; + $value = $this->endpoint; + } + + if (str($value)->contains('your-objectstorage.com') && ! isset($this->bucket)) { + $this->bucket = str($value)->after('//')->before('.'); + } elseif (str($value)->contains('your-objectstorage.com')) { + $this->bucket = $this->bucket ?: str($value)->after('//')->before('.'); } } @@ -59,7 +61,7 @@ class Create extends Component { try { $this->validate(); - $this->storage = new S3Storage(); + $this->storage = new S3Storage; $this->storage->name = $this->name; $this->storage->description = $this->description ?? null; $this->storage->region = $this->region; diff --git a/app/Livewire/Subscription/Actions.php b/app/Livewire/Subscription/Actions.php index db1f565a6..1388d3244 100644 --- a/app/Livewire/Subscription/Actions.php +++ b/app/Livewire/Subscription/Actions.php @@ -3,7 +3,6 @@ namespace App\Livewire\Subscription; use App\Models\Team; -use Illuminate\Support\Facades\Http; use Livewire\Component; class Actions extends Component @@ -15,70 +14,6 @@ class Actions extends Component $this->server_limits = Team::serverLimit(); } - public function cancel() - { - try { - $subscription_id = currentTeam()->subscription->lemon_subscription_id; - if (! $subscription_id) { - throw new \Exception('No subscription found'); - } - $response = Http::withHeaders([ - 'Accept' => 'application/vnd.api+json', - 'Content-Type' => 'application/vnd.api+json', - 'Authorization' => 'Bearer '.config('subscription.lemon_squeezy_api_key'), - ])->delete('https://api.lemonsqueezy.com/v1/subscriptions/'.$subscription_id); - $json = $response->json(); - if ($response->failed()) { - $error = data_get($json, 'errors.0.status'); - if ($error === '404') { - throw new \Exception('Subscription not found.'); - } - throw new \Exception(data_get($json, 'errors.0.title', 'Something went wrong. Please try again later.')); - } else { - $this->dispatch('success', 'Subscription cancelled successfully. Reloading in 5s.'); - $this->dispatch('reloadWindow', 5000); - } - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function resume() - { - try { - $subscription_id = currentTeam()->subscription->lemon_subscription_id; - if (! $subscription_id) { - throw new \Exception('No subscription found'); - } - $response = Http::withHeaders([ - 'Accept' => 'application/vnd.api+json', - 'Content-Type' => 'application/vnd.api+json', - 'Authorization' => 'Bearer '.config('subscription.lemon_squeezy_api_key'), - ])->patch('https://api.lemonsqueezy.com/v1/subscriptions/'.$subscription_id, [ - 'data' => [ - 'type' => 'subscriptions', - 'id' => $subscription_id, - 'attributes' => [ - 'cancelled' => false, - ], - ], - ]); - $json = $response->json(); - if ($response->failed()) { - $error = data_get($json, 'errors.0.status'); - if ($error === '404') { - throw new \Exception('Subscription not found.'); - } - throw new \Exception(data_get($json, 'errors.0.title', 'Something went wrong. Please try again later.')); - } else { - $this->dispatch('success', 'Subscription resumed successfully. Reloading in 5s.'); - $this->dispatch('reloadWindow', 5000); - } - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - public function stripeCustomerPortal() { $session = getStripeCustomerPortalSession(currentTeam()); diff --git a/app/Livewire/Subscription/Index.php b/app/Livewire/Subscription/Index.php index c072352fe..df450cf7e 100644 --- a/app/Livewire/Subscription/Index.php +++ b/app/Livewire/Subscription/Index.php @@ -23,7 +23,7 @@ class Index extends Component if (data_get(currentTeam(), 'subscription') && isSubscriptionActive()) { return redirect()->route('subscription.show'); } - $this->settings = InstanceSettings::get(); + $this->settings = instanceSettings(); $this->alreadySubscribed = currentTeam()->subscription()->exists(); } diff --git a/app/Livewire/Subscription/PricingPlans.php b/app/Livewire/Subscription/PricingPlans.php index 9bc11d862..6b2d3fb36 100644 --- a/app/Livewire/Subscription/PricingPlans.php +++ b/app/Livewire/Subscription/PricingPlans.php @@ -2,55 +2,23 @@ namespace App\Livewire\Subscription; +use Illuminate\Support\Facades\Auth; use Livewire\Component; use Stripe\Checkout\Session; use Stripe\Stripe; class PricingPlans extends Component { - public bool $isTrial = false; - - public function mount() - { - $this->isTrial = ! data_get(currentTeam(), 'subscription.stripe_trial_already_ended'); - if (config('constants.limits.trial_period') == 0) { - $this->isTrial = false; - } - } - public function subscribeStripe($type) { - $team = currentTeam(); Stripe::setApiKey(config('subscription.stripe_api_key')); - switch ($type) { - case 'basic-monthly': - $priceId = config('subscription.stripe_price_id_basic_monthly'); - break; - case 'basic-yearly': - $priceId = config('subscription.stripe_price_id_basic_yearly'); - break; - case 'pro-monthly': - $priceId = config('subscription.stripe_price_id_pro_monthly'); - break; - case 'pro-yearly': - $priceId = config('subscription.stripe_price_id_pro_yearly'); - break; - case 'ultimate-monthly': - $priceId = config('subscription.stripe_price_id_ultimate_monthly'); - break; - case 'ultimate-yearly': - $priceId = config('subscription.stripe_price_id_ultimate_yearly'); - break; - case 'dynamic-monthly': - $priceId = config('subscription.stripe_price_id_dynamic_monthly'); - break; - case 'dynamic-yearly': - $priceId = config('subscription.stripe_price_id_dynamic_yearly'); - break; - default: - $priceId = config('subscription.stripe_price_id_basic_monthly'); - break; - } + + $priceId = match ($type) { + 'dynamic-monthly' => config('subscription.stripe_price_id_dynamic_monthly'), + 'dynamic-yearly' => config('subscription.stripe_price_id_dynamic_yearly'), + default => config('subscription.stripe_price_id_dynamic_monthly'), + }; + if (! $priceId) { $this->dispatch('error', 'Price ID not found! Please contact the administrator.'); @@ -59,10 +27,14 @@ class PricingPlans extends Component $payload = [ 'allow_promotion_codes' => true, 'billing_address_collection' => 'required', - 'client_reference_id' => auth()->user()->id.':'.currentTeam()->id, + 'client_reference_id' => Auth::id().':'.currentTeam()->id, 'line_items' => [[ 'price' => $priceId, - 'quantity' => 1, + 'adjustable_quantity' => [ + 'enabled' => true, + 'minimum' => 2, + ], + 'quantity' => 2, ]], 'tax_id_collection' => [ 'enabled' => true, @@ -70,39 +42,18 @@ class PricingPlans extends Component 'automatic_tax' => [ 'enabled' => true, ], - + 'subscription_data' => [ + 'metadata' => [ + 'user_id' => Auth::id(), + 'team_id' => currentTeam()->id, + ], + ], + 'payment_method_collection' => 'if_required', 'mode' => 'subscription', 'success_url' => route('dashboard', ['success' => true]), 'cancel_url' => route('subscription.index', ['cancelled' => true]), ]; - if (str($type)->contains('ultimate')) { - $payload['line_items'][0]['adjustable_quantity'] = [ - 'enabled' => true, - 'minimum' => 10, - ]; - $payload['line_items'][0]['quantity'] = 10; - } - if (str($type)->contains('dynamic')) { - $payload['line_items'][0]['adjustable_quantity'] = [ - 'enabled' => true, - 'minimum' => 2, - ]; - $payload['line_items'][0]['quantity'] = 2; - } - if (! data_get($team, 'subscription.stripe_trial_already_ended')) { - if (config('constants.limits.trial_period') > 0) { - $payload['subscription_data'] = [ - 'trial_period_days' => config('constants.limits.trial_period'), - 'trial_settings' => [ - 'end_behavior' => [ - 'missing_payment_method' => 'cancel', - ], - ], - ]; - } - $payload['payment_method_collection'] = 'if_required'; - } $customer = currentTeam()->subscription?->stripe_customer_id ?? null; if ($customer) { $payload['customer'] = $customer; @@ -110,7 +61,7 @@ class PricingPlans extends Component 'name' => 'auto', ]; } else { - $payload['customer_email'] = auth()->user()->email; + $payload['customer_email'] = Auth::user()->email; } $session = Session::create($payload); diff --git a/app/Livewire/Tags/Deployments.php b/app/Livewire/Tags/Deployments.php index 270aa176a..e4afa5b60 100644 --- a/app/Livewire/Tags/Deployments.php +++ b/app/Livewire/Tags/Deployments.php @@ -7,19 +7,19 @@ use Livewire\Component; class Deployments extends Component { - public $deployments_per_tag_per_server = []; + public $deploymentsPerTagPerServer = []; - public $resource_ids = []; + public $resourceIds = []; public function render() { return view('livewire.tags.deployments'); } - public function get_deployments() + public function getDeployments() { try { - $this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $this->resource_ids)->get([ + $this->deploymentsPerTagPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $this->resourceIds)->get([ 'id', 'application_id', 'application_name', @@ -29,7 +29,7 @@ class Deployments extends Component 'server_id', 'status', ])->sortBy('id')->groupBy('server_name')->toArray(); - $this->dispatch('deployments', $this->deployments_per_tag_per_server); + $this->dispatch('deployments', $this->deploymentsPerTagPerServer); } catch (\Exception $e) { return handleError($e, $this); } diff --git a/app/Livewire/Tags/Index.php b/app/Livewire/Tags/Index.php deleted file mode 100644 index 91e15835f..000000000 --- a/app/Livewire/Tags/Index.php +++ /dev/null @@ -1,79 +0,0 @@ - 'update_deployments']; - - public function update_deployments($deployments) - { - $this->deployments_per_tag_per_server = $deployments; - } - - public function tag_updated() - { - if ($this->tag == '') { - return; - } - $tag = $this->tags->where('name', $this->tag)->first(); - if (! $tag) { - $this->dispatch('error', "Tag ({$this->tag}) not found."); - $this->tag = ''; - - return; - } - $this->webhook = generatTagDeployWebhook($tag->name); - $this->applications = $tag->applications()->get(); - $this->services = $tag->services()->get(); - } - - public function redeploy_all() - { - try { - $this->applications->each(function ($resource) { - $deploy = new Deploy(); - $deploy->deploy_resource($resource); - }); - $this->services->each(function ($resource) { - $deploy = new Deploy(); - $deploy->deploy_resource($resource); - }); - $this->dispatch('success', 'Mass deployment started.'); - } catch (\Exception $e) { - return handleError($e, $this); - } - } - - public function mount() - { - $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name'); - if ($this->tag) { - $this->tag_updated(); - } - } - - public function render() - { - return view('livewire.tags.index'); - } -} diff --git a/app/Livewire/Tags/Show.php b/app/Livewire/Tags/Show.php index f4ecc67a0..fc5b13374 100644 --- a/app/Livewire/Tags/Show.php +++ b/app/Livewire/Tags/Show.php @@ -2,44 +2,60 @@ namespace App\Livewire\Tags; -use App\Http\Controllers\Api\Deploy; +use App\Http\Controllers\Api\DeployController; use App\Models\ApplicationDeploymentQueue; use App\Models\Tag; +use Illuminate\Support\Collection; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Title; use Livewire\Component; +#[Title('Tags | Coolify')] class Show extends Component { - public $tags; + #[Locked] + public ?string $tagName = null; - public Tag $tag; + #[Locked] + public ?Collection $tags = null; - public $applications; + #[Locked] + public ?Tag $tag = null; - public $services; + #[Locked] + public ?Collection $applications = null; - public $webhook = null; + #[Locked] + public ?Collection $services = null; - public $deployments_per_tag_per_server = []; + #[Locked] + public ?string $webhook = null; + + #[Locked] + public ?array $deploymentsPerTagPerServer = null; public function mount() { - $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name'); - $tag = $this->tags->where('name', request()->tag_name)->first(); - if (! $tag) { - return redirect()->route('tags.index'); + try { + $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name'); + if (str($this->tagName)->isNotEmpty()) { + $tag = $this->tags->where('name', $this->tagName)->first(); + $this->webhook = generateTagDeployWebhook($tag->name); + $this->applications = $tag->applications()->get(); + $this->services = $tag->services()->get(); + $this->tag = $tag; + $this->getDeployments(); + } + } catch (\Exception $e) { + return handleError($e, $this); } - $this->webhook = generatTagDeployWebhook($tag->name); - $this->applications = $tag->applications()->get(); - $this->services = $tag->services()->get(); - $this->tag = $tag; - $this->get_deployments(); } - public function get_deployments() + public function getDeployments() { try { $resource_ids = $this->applications->pluck('id'); - $this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $resource_ids)->get([ + $this->deploymentsPerTagPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $resource_ids)->get([ 'id', 'application_id', 'application_name', @@ -54,16 +70,16 @@ class Show extends Component } } - public function redeploy_all() + public function redeployAll() { try { $message = collect([]); $this->applications->each(function ($resource) use ($message) { - $deploy = new Deploy(); + $deploy = new DeployController; $message->push($deploy->deploy_resource($resource)); }); $this->services->each(function ($resource) use ($message) { - $deploy = new Deploy(); + $deploy = new DeployController; $message->push($deploy->deploy_resource($resource)); }); $this->dispatch('success', 'Mass deployment started.'); diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php index 97d4fcdbf..cfb47d9d8 100644 --- a/app/Livewire/Team/AdminView.php +++ b/app/Livewire/Team/AdminView.php @@ -2,8 +2,11 @@ namespace App\Livewire\Team; +use App\Models\InstanceSettings; use App\Models\Team; use App\Models\User; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; use Livewire\Component; class AdminView extends Component @@ -56,54 +59,54 @@ class AdminView extends Component foreach ($servers as $server) { $resources = $server->definedResources(); foreach ($resources as $resource) { - ray('Deleting resource: '.$resource->name); $resource->forceDelete(); } - ray('Deleting server: '.$server->name); $server->forceDelete(); } $projects = $team->projects; foreach ($projects as $project) { - ray('Deleting project: '.$project->name); $project->forceDelete(); } $team->members()->detach($user->id); - ray('Deleting team: '.$team->name); $team->delete(); } - public function delete($id) + public function delete($id, $password) { + if (! isInstanceAdmin()) { + return redirect()->route('dashboard'); + } + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); + + return; + } + } if (! auth()->user()->isInstanceAdmin()) { return $this->dispatch('error', 'You are not authorized to delete users'); } $user = User::find($id); $teams = $user->teams; foreach ($teams as $team) { - ray($team->name); $user_alone_in_team = $team->members->count() === 1; if ($team->id === 0) { if ($user_alone_in_team) { - ray('user is alone in the root team, do nothing'); - return $this->dispatch('error', 'User is alone in the root team, cannot delete'); } } if ($user_alone_in_team) { - ray('user is alone in the team'); $this->finalizeDeletion($user, $team); continue; } - ray('user is not alone in the team'); if ($user->isOwner()) { $found_other_owner_or_admin = $team->members->filter(function ($member) { return $member->pivot->role === 'owner' || $member->pivot->role === 'admin'; })->where('id', '!=', $user->id)->first(); if ($found_other_owner_or_admin) { - ray('found other owner or admin'); $team->members()->detach($user->id); continue; @@ -112,24 +115,19 @@ class AdminView extends Component return $member->pivot->role === 'member'; })->first(); if ($found_other_member_who_is_not_owner) { - ray('found other member who is not owner'); $found_other_member_who_is_not_owner->pivot->role = 'owner'; $found_other_member_who_is_not_owner->pivot->save(); $team->members()->detach($user->id); } else { - // This should never happen as if the user is the only member in the team, the team should be deleted already. - ray('found no other member who is not owner'); $this->finalizeDeletion($user, $team); } continue; } } else { - ray('user is not owner'); $team->members()->detach($user->id); } } - ray('Deleting user: '.$user->name); $user->delete(); $this->getUsers(); } diff --git a/app/Livewire/Team/Create.php b/app/Livewire/Team/Create.php index 992833da5..f805d6122 100644 --- a/app/Livewire/Team/Create.php +++ b/app/Livewire/Team/Create.php @@ -3,28 +3,21 @@ namespace App\Livewire\Team; use App\Models\Team; +use Livewire\Attributes\Validate; use Livewire\Component; class Create extends Component { + #[Validate(['required', 'min:3', 'max:255'])] public string $name = ''; + #[Validate(['nullable', 'min:3', 'max:255'])] public ?string $description = null; - protected $rules = [ - 'name' => 'required|min:3|max:255', - 'description' => 'nullable|min:3|max:255', - ]; - - protected $validationAttributes = [ - 'name' => 'name', - 'description' => 'description', - ]; - public function submit() { - $this->validate(); try { + $this->validate(); $team = Team::create([ 'name' => $this->name, 'description' => $this->description, diff --git a/app/Livewire/Team/Index.php b/app/Livewire/Team/Index.php index 45600dbfe..0972e7364 100644 --- a/app/Livewire/Team/Index.php +++ b/app/Livewire/Team/Index.php @@ -4,6 +4,7 @@ namespace App\Livewire\Team; use App\Models\Team; use App\Models\TeamInvitation; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Livewire\Component; @@ -55,7 +56,7 @@ class Index extends Component $currentTeam->delete(); $currentTeam->members->each(function ($user) use ($currentTeam) { - if ($user->id === auth()->user()->id) { + if ($user->id === Auth::id()) { return; } $user->teams()->detach($currentTeam); diff --git a/app/Livewire/Team/Invitations.php b/app/Livewire/Team/Invitations.php index 6a32a1d16..93432efc8 100644 --- a/app/Livewire/Team/Invitations.php +++ b/app/Livewire/Team/Invitations.php @@ -13,17 +13,18 @@ class Invitations extends Component public function deleteInvitation(int $invitation_id) { - $initiation_found = TeamInvitation::find($invitation_id); - if (! $initiation_found) { + try { + $initiation_found = TeamInvitation::ownedByCurrentTeam()->findOrFail($invitation_id); + $initiation_found->delete(); + $this->refreshInvitations(); + $this->dispatch('success', 'Invitation revoked.'); + } catch (\Exception) { return $this->dispatch('error', 'Invitation not found.'); } - $initiation_found->delete(); - $this->refreshInvitations(); - $this->dispatch('success', 'Invitation revoked.'); } public function refreshInvitations() { - $this->invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get(); + $this->invitations = TeamInvitation::ownedByCurrentTeam()->get(); } } diff --git a/app/Livewire/Team/InviteLink.php b/app/Livewire/Team/InviteLink.php index cc69e6650..25f8a1ff5 100644 --- a/app/Livewire/Team/InviteLink.php +++ b/app/Livewire/Team/InviteLink.php @@ -41,6 +41,9 @@ class InviteLink extends Component { try { $this->validate(); + if (auth()->user()->role() === 'admin' && $this->role === 'owner') { + throw new \Exception('Admins cannot invite owners.'); + } $member_emails = currentTeam()->members()->get()->pluck('email'); if ($member_emails->contains($this->email)) { return handleError(livewire: $this, customErrorMessage: "$this->email is already a member of ".currentTeam()->name.'.'); @@ -52,7 +55,7 @@ class InviteLink extends Component if (is_null($user)) { $password = Str::password(); $user = User::create([ - 'name' => Str::of($this->email)->before('@'), + 'name' => str($this->email)->before('@'), 'email' => $this->email, 'password' => Hash::make($password), 'force_password_reset' => true, @@ -79,7 +82,7 @@ class InviteLink extends Component 'via' => $sendEmail ? 'email' : 'link', ]); if ($sendEmail) { - $mail = new MailMessage(); + $mail = new MailMessage; $mail->view('emails.invitation-link', [ 'team' => currentTeam()->name, 'invitation_link' => $link, diff --git a/app/Livewire/Team/Member.php b/app/Livewire/Team/Member.php index 680cb901b..890d640a0 100644 --- a/app/Livewire/Team/Member.php +++ b/app/Livewire/Team/Member.php @@ -2,6 +2,7 @@ namespace App\Livewire\Team; +use App\Enums\Role; use App\Models\User; use Illuminate\Support\Facades\Cache; use Livewire\Component; @@ -12,29 +13,66 @@ class Member extends Component public function makeAdmin() { - $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'admin']); - $this->dispatch('reloadWindow'); + try { + if (Role::from(auth()->user()->role())->lt(Role::ADMIN) + || Role::from($this->getMemberRole())->gt(auth()->user()->role())) { + throw new \Exception('You are not authorized to perform this action.'); + } + $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => Role::ADMIN->value]); + $this->dispatch('reloadWindow'); + } catch (\Exception $e) { + $this->dispatch('error', $e->getMessage()); + } } public function makeOwner() { - $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'owner']); - $this->dispatch('reloadWindow'); + try { + if (Role::from(auth()->user()->role())->lt(Role::OWNER) + || Role::from($this->getMemberRole())->gt(auth()->user()->role())) { + throw new \Exception('You are not authorized to perform this action.'); + } + $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => Role::OWNER->value]); + $this->dispatch('reloadWindow'); + } catch (\Exception $e) { + $this->dispatch('error', $e->getMessage()); + } } public function makeReadonly() { - $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'member']); - $this->dispatch('reloadWindow'); + try { + if (Role::from(auth()->user()->role())->lt(Role::ADMIN) + || Role::from($this->getMemberRole())->gt(auth()->user()->role())) { + throw new \Exception('You are not authorized to perform this action.'); + } + $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => Role::MEMBER->value]); + $this->dispatch('reloadWindow'); + } catch (\Exception $e) { + $this->dispatch('error', $e->getMessage()); + } } public function remove() { - $this->member->teams()->detach(currentTeam()); - Cache::forget("team:{$this->member->id}"); - Cache::remember('team:'.$this->member->id, 3600, function () { - return $this->member->teams()->first(); - }); - $this->dispatch('reloadWindow'); + try { + if (Role::from(auth()->user()->role())->lt(Role::ADMIN) + || Role::from($this->getMemberRole())->gt(auth()->user()->role())) { + throw new \Exception('You are not authorized to perform this action.'); + } + $this->member->teams()->detach(currentTeam()); + Cache::forget("team:{$this->member->id}"); + Cache::remember('team:'.$this->member->id, 3600, function () { + return $this->member->teams()->first(); + }); + $this->dispatch('reloadWindow'); + } catch (\Exception $e) { + $this->dispatch('error', $e->getMessage()); + } + } + + private function getMemberRole() + { + return $this->member->teams()->where('teams.id', currentTeam()->id)->first()?->pivot?->role; } } diff --git a/app/Livewire/Terminal/Index.php b/app/Livewire/Terminal/Index.php new file mode 100644 index 000000000..a24a237c5 --- /dev/null +++ b/app/Livewire/Terminal/Index.php @@ -0,0 +1,88 @@ +user()->isAdmin()) { + abort(403); + } + $this->servers = Server::isReachable()->get(); + } + + public function loadContainers() + { + try { + $this->containers = $this->getAllActiveContainers(); + } catch (\Exception $e) { + return handleError($e, $this); + } finally { + $this->isLoadingContainers = false; + } + } + + private function getAllActiveContainers() + { + return collect($this->servers)->flatMap(function ($server) { + if (! $server->isFunctional()) { + return []; + } + + return $server->loadAllContainers()->map(function ($container) use ($server) { + $state = data_get_str($container, 'State')->lower(); + if ($state->contains('running')) { + return [ + 'name' => data_get($container, 'Names'), + 'connection_name' => data_get($container, 'Names'), + 'uuid' => data_get($container, 'Names'), + 'status' => data_get_str($container, 'State')->lower(), + 'server' => $server, + 'server_uuid' => $server->uuid, + ]; + } + + return null; + })->filter(); + }); + } + + public function updatedSelectedUuid() + { + $this->connectToContainer(); + } + + #[On('connectToContainer')] + public function connectToContainer() + { + if ($this->selected_uuid === 'default') { + $this->dispatch('error', 'Please select a server or a container.'); + + return; + } + $container = collect($this->containers)->firstWhere('uuid', $this->selected_uuid); + $this->dispatch('send-terminal-command', + isset($container), + $container['connection_name'] ?? $this->selected_uuid, + $container['server_uuid'] ?? $this->selected_uuid + ); + } + + public function render() + { + return view('livewire.terminal.index'); + } +} diff --git a/app/Livewire/Upgrade.php b/app/Livewire/Upgrade.php index 7ad7e9523..e50085c64 100644 --- a/app/Livewire/Upgrade.php +++ b/app/Livewire/Upgrade.php @@ -3,6 +3,7 @@ namespace App\Livewire; use App\Actions\Server\UpdateCoolify; +use App\Models\InstanceSettings; use Livewire\Component; class Upgrade extends Component @@ -15,13 +16,18 @@ class Upgrade extends Component public string $latestVersion = ''; + protected $listeners = ['updateAvailable' => 'checkUpdate']; + public function checkUpdate() { - $this->latestVersion = get_latest_version_of_coolify(); - $currentVersion = config('version'); - version_compare($currentVersion, $this->latestVersion, '<') ? $this->isUpgradeAvailable = true : $this->isUpgradeAvailable = false; - if (isDev()) { - $this->isUpgradeAvailable = true; + try { + $this->latestVersion = get_latest_version_of_coolify(); + $this->isUpgradeAvailable = data_get(InstanceSettings::get(), 'new_version_available', false); + if (isDev()) { + $this->isUpgradeAvailable = true; + } + } catch (\Throwable $e) { + return handleError($e, $this); } } diff --git a/app/Livewire/VerifyEmail.php b/app/Livewire/VerifyEmail.php index d1f79c835..fab3265b6 100644 --- a/app/Livewire/VerifyEmail.php +++ b/app/Livewire/VerifyEmail.php @@ -15,10 +15,7 @@ class VerifyEmail extends Component $this->rateLimit(1, 300); auth()->user()->sendVerificationEmail(); $this->dispatch('success', 'Email verification link sent!'); - } catch (\Exception $e) { - ray($e); - return handleError($e, $this); } } diff --git a/app/Models/Application.php b/app/Models/Application.php index e3d05bc2c..dd7c446b5 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -6,50 +6,159 @@ use App\Enums\ApplicationDeploymentStatus; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Process\InvokedProcess; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Process; +use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; +use OpenApi\Attributes as OA; use RuntimeException; use Spatie\Activitylog\Models\Activity; use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; use Visus\Cuid2\Cuid2; +#[OA\Schema( + description: 'Application model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer', 'description' => 'The application identifier in the database.'], + 'description' => ['type' => 'string', 'nullable' => true, 'description' => 'The application description.'], + 'repository_project_id' => ['type' => 'integer', 'nullable' => true, 'description' => 'The repository project identifier.'], + 'uuid' => ['type' => 'string', 'description' => 'The application UUID.'], + 'name' => ['type' => 'string', 'description' => 'The application name.'], + 'fqdn' => ['type' => 'string', 'nullable' => true, 'description' => 'The application domains.'], + 'config_hash' => ['type' => 'string', 'description' => 'Configuration hash.'], + 'git_repository' => ['type' => 'string', 'description' => 'Git repository URL.'], + 'git_branch' => ['type' => 'string', 'description' => 'Git branch.'], + 'git_commit_sha' => ['type' => 'string', 'description' => 'Git commit SHA.'], + 'git_full_url' => ['type' => 'string', 'nullable' => true, 'description' => 'Git full URL.'], + 'docker_registry_image_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker registry image name.'], + 'docker_registry_image_tag' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker registry image tag.'], + 'build_pack' => ['type' => 'string', 'description' => 'Build pack.', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose']], + 'static_image' => ['type' => 'string', 'description' => 'Static image used when static site is deployed.'], + 'install_command' => ['type' => 'string', 'description' => 'Install command.'], + 'build_command' => ['type' => 'string', 'description' => 'Build command.'], + 'start_command' => ['type' => 'string', 'description' => 'Start command.'], + 'ports_exposes' => ['type' => 'string', 'description' => 'Ports exposes.'], + 'ports_mappings' => ['type' => 'string', 'nullable' => true, 'description' => 'Ports mappings.'], + 'base_directory' => ['type' => 'string', 'description' => 'Base directory for all commands.'], + 'publish_directory' => ['type' => 'string', 'description' => 'Publish directory.'], + 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'], + 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'], + 'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'], + 'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'], + 'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'], + 'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'], + 'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'], + 'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'], + 'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'], + 'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'], + 'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'], + 'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'], + 'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'], + 'status' => ['type' => 'string', 'description' => 'Application status.'], + 'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'], + 'destination_type' => ['type' => 'string', 'description' => 'Destination type.'], + 'destination_id' => ['type' => 'integer', 'description' => 'Destination identifier.'], + 'source_id' => ['type' => 'integer', 'nullable' => true, 'description' => 'Source identifier.'], + 'private_key_id' => ['type' => 'integer', 'nullable' => true, 'description' => 'Private key identifier.'], + 'environment_id' => ['type' => 'integer', 'description' => 'Environment identifier.'], + 'dockerfile' => ['type' => 'string', 'nullable' => true, 'description' => 'Dockerfile content. Used for dockerfile build pack.'], + 'dockerfile_location' => ['type' => 'string', 'description' => 'Dockerfile location.'], + 'custom_labels' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom labels.'], + 'dockerfile_target_build' => ['type' => 'string', 'nullable' => true, 'description' => 'Dockerfile target build.'], + 'manual_webhook_secret_github' => ['type' => 'string', 'nullable' => true, 'description' => 'Manual webhook secret for GitHub.'], + 'manual_webhook_secret_gitlab' => ['type' => 'string', 'nullable' => true, 'description' => 'Manual webhook secret for GitLab.'], + 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'nullable' => true, 'description' => 'Manual webhook secret for Bitbucket.'], + 'manual_webhook_secret_gitea' => ['type' => 'string', 'nullable' => true, 'description' => 'Manual webhook secret for Gitea.'], + 'docker_compose_location' => ['type' => 'string', 'description' => 'Docker compose location.'], + 'docker_compose' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose content. Used for docker compose build pack.'], + 'docker_compose_raw' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose raw content.'], + 'docker_compose_domains' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose domains.'], + 'docker_compose_custom_start_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose custom start command.'], + 'docker_compose_custom_build_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose custom build command.'], + 'swarm_replicas' => ['type' => 'integer', 'nullable' => true, 'description' => 'Swarm replicas. Only used for swarm deployments.'], + 'swarm_placement_constraints' => ['type' => 'string', 'nullable' => true, 'description' => 'Swarm placement constraints. Only used for swarm deployments.'], + 'custom_docker_run_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom docker run options.'], + 'post_deployment_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Post deployment command.'], + 'post_deployment_command_container' => ['type' => 'string', 'nullable' => true, 'description' => 'Post deployment command container.'], + 'pre_deployment_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Pre deployment command.'], + 'pre_deployment_command_container' => ['type' => 'string', 'nullable' => true, 'description' => 'Pre deployment command container.'], + 'watch_paths' => ['type' => 'string', 'nullable' => true, 'description' => 'Watch paths.'], + 'custom_healthcheck_found' => ['type' => 'boolean', 'description' => 'Custom healthcheck found.'], + 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], + 'created_at' => ['type' => 'string', 'format' => 'date-time', 'description' => 'The date and time when the application was created.'], + 'updated_at' => ['type' => 'string', 'format' => 'date-time', 'description' => 'The date and time when the application was last updated.'], + 'deleted_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true, 'description' => 'The date and time when the application was deleted.'], + 'compose_parsing_version' => ['type' => 'string', 'description' => 'How Coolify parse the compose file.'], + 'custom_nginx_configuration' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom Nginx configuration base64 encoded.'], + ] +)] + class Application extends BaseModel { use SoftDeletes; + private static $parserVersion = '4'; + protected $guarded = []; + protected $appends = ['server_status']; + protected static function booted() { static::saving(function ($application) { - if ($application->fqdn == '') { - $application->fqdn = null; + $payload = []; + if ($application->isDirty('fqdn')) { + if ($application->fqdn === '') { + $application->fqdn = null; + } + $payload['fqdn'] = $application->fqdn; + } + if ($application->isDirty('install_command')) { + $payload['install_command'] = str($application->install_command)->trim(); + } + if ($application->isDirty('build_command')) { + $payload['build_command'] = str($application->build_command)->trim(); + } + if ($application->isDirty('start_command')) { + $payload['start_command'] = str($application->start_command)->trim(); + } + if ($application->isDirty('base_directory')) { + $payload['base_directory'] = str($application->base_directory)->trim(); + } + if ($application->isDirty('publish_directory')) { + $payload['publish_directory'] = str($application->publish_directory)->trim(); + } + if ($application->isDirty('status')) { + $payload['last_online_at'] = now(); + } + if ($application->isDirty('custom_nginx_configuration')) { + if ($application->custom_nginx_configuration === '') { + $payload['custom_nginx_configuration'] = null; + } + } + if (count($payload) > 0) { + $application->forceFill($payload); } - $application->forceFill([ - 'fqdn' => $application->fqdn, - 'install_command' => Str::of($application->install_command)->trim(), - 'build_command' => Str::of($application->build_command)->trim(), - 'start_command' => Str::of($application->start_command)->trim(), - 'base_directory' => Str::of($application->base_directory)->trim(), - 'publish_directory' => Str::of($application->publish_directory)->trim(), - ]); }); static::created(function ($application) { ApplicationSetting::create([ 'application_id' => $application->id, ]); + $application->compose_parsing_version = self::$parserVersion; + $application->save(); }); - static::deleting(function ($application) { + static::forceDeleting(function ($application) { $application->update(['fqdn' => null]); $application->settings()->delete(); - $storages = $application->persistentStorages()->get(); - $server = data_get($application, 'destination.server'); - if ($server) { - foreach ($storages as $storage) { - instant_remote_process(["docker volume rm -f $storage->name"], $server, false); - } - } $application->persistentStorages()->delete(); $application->environment_variables()->delete(); $application->environment_variables_preview()->delete(); @@ -57,19 +166,108 @@ class Application extends BaseModel $task->delete(); } $application->tags()->detach(); + $application->previews()->delete(); + foreach ($application->deployment_queue as $deployment) { + $deployment->delete(); + } }); } + public static function ownedByCurrentTeamAPI(int $teamId) + { + return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name'); + } + + public static function ownedByCurrentTeam() + { + return Application::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + + public function getContainersToStop(bool $previewDeployments = false): array + { + $containers = $previewDeployments + ? getCurrentApplicationContainerStatus($this->destination->server, $this->id, includePullrequests: true) + : getCurrentApplicationContainerStatus($this->destination->server, $this->id, 0); + + return $containers->pluck('Names')->toArray(); + } + + public function stopContainers(array $containerNames, $server, int $timeout = 600) + { + $processes = []; + foreach ($containerNames as $containerName) { + $processes[$containerName] = $this->stopContainer($containerName, $server, $timeout); + } + + $startTime = time(); + while (count($processes) > 0) { + $finishedProcesses = array_filter($processes, function ($process) { + return ! $process->running(); + }); + foreach ($finishedProcesses as $containerName => $process) { + unset($processes[$containerName]); + $this->removeContainer($containerName, $server); + } + + if (time() - $startTime >= $timeout) { + $this->forceStopRemainingContainers(array_keys($processes), $server); + break; + } + + usleep(100000); + } + } + + public function stopContainer(string $containerName, $server, int $timeout): InvokedProcess + { + return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); + } + + public function removeContainer(string $containerName, $server) + { + instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false); + } + + public function forceStopRemainingContainers(array $containerNames, $server) + { + foreach ($containerNames as $containerName) { + instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false); + $this->removeContainer($containerName, $server); + } + } + public function delete_configurations() { $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); if (str($workdir)->endsWith($this->uuid)) { - ray('Deleting workdir'); instant_remote_process(['rm -rf '.$this->workdir()], $server, false); } } + public function delete_volumes(?Collection $persistentStorages) + { + if ($this->build_pack === 'dockercompose') { + $server = data_get($this, 'destination.server'); + instant_remote_process(["cd {$this->dirOnServer()} && docker compose down -v"], $server, false); + } else { + if ($persistentStorages->count() === 0) { + return; + } + $server = data_get($this, 'destination.server'); + foreach ($persistentStorages as $storage) { + instant_remote_process(["docker volume rm -f $storage->name"], $server, false); + } + } + } + + public function delete_connected_networks($uuid) + { + $server = data_get($this, 'destination.server'); + instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false); + instant_remote_process(["docker network rm {$uuid}"], $server, false); + } + public function additional_servers() { return $this->belongsToMany(Server::class, 'additional_destinations') @@ -131,12 +329,24 @@ class Application extends BaseModel public function failedTaskLink($task_uuid) { if (data_get($this, 'environment.project.uuid')) { - return route('project.application.scheduled-tasks', [ + $route = route('project.application.scheduled-tasks', [ 'project_uuid' => data_get($this, 'environment.project.uuid'), 'environment_name' => data_get($this, 'environment.name'), 'application_uuid' => data_get($this, 'uuid'), 'task_uuid' => $task_uuid, ]); + $settings = instanceSettings(); + if (data_get($settings, 'fqdn')) { + $url = Url::fromString($route); + $url = $url->withPort(null); + $fqdn = data_get($settings, 'fqdn'); + $fqdn = str_replace(['http://', 'https://'], '', $fqdn); + $url = $url->withHost($fqdn); + + return $url->__toString(); + } + + return $route; } return null; @@ -174,12 +384,20 @@ class Application extends BaseModel return Attribute::make( get: function () { if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) { + if (str($this->git_repository)->contains('bitbucket')) { + return "{$this->source->html_url}/{$this->git_repository}/src/{$this->git_branch}"; + } + return "{$this->source->html_url}/{$this->git_repository}/tree/{$this->git_branch}"; } // Convert the SSH URL to HTTPS URL if (strpos($this->git_repository, 'git@') === 0) { $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); + if (str($this->git_repository)->contains('bitbucket')) { + return "https://{$git_repository}/src/{$this->git_branch}"; + } + return "https://{$git_repository}/tree/{$this->git_branch}"; } @@ -228,18 +446,13 @@ class Application extends BaseModel public function gitCommitLink($link): string { - if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) { + if (! is_null(data_get($this, 'source.html_url')) && ! is_null(data_get($this, 'git_repository')) && ! is_null(data_get($this, 'git_branch'))) { if (str($this->source->html_url)->contains('bitbucket')) { return "{$this->source->html_url}/{$this->git_repository}/commits/{$link}"; } return "{$this->source->html_url}/{$this->git_repository}/commit/{$link}"; } - if (strpos($this->git_repository, 'git@') === 0) { - $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); - - return "https://{$git_repository}/commit/{$link}"; - } if (str($this->git_repository)->contains('bitbucket')) { $git_repository = str_replace('.git', '', $this->git_repository); $url = Url::fromString($git_repository); @@ -248,6 +461,14 @@ class Application extends BaseModel return $url->__toString(); } + if (strpos($this->git_repository, 'git@') === 0) { + $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); + if (data_get($this, 'source.html_url')) { + return "{$this->source->html_url}/{$git_repository}/commit/{$link}"; + } + + return "{$git_repository}/commit/{$link}"; + } return $this->git_repository; } @@ -286,23 +507,6 @@ class Application extends BaseModel ); } - public function dockerComposePrLocation(): Attribute - { - return Attribute::make( - set: function ($value) { - if (is_null($value) || $value === '') { - return '/docker-compose.yaml'; - } else { - if ($value !== '/') { - return Str::start(Str::replaceEnd('/', '', $value), '/'); - } - - return Str::start($value, '/'); - } - } - ); - } - public function baseDirectory(): Attribute { return Attribute::make( @@ -327,6 +531,11 @@ class Application extends BaseModel ); } + public function isRunning() + { + return (bool) str($this->status)->startsWith('running'); + } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); @@ -337,6 +546,28 @@ class Application extends BaseModel return $this->getRawOriginal('status'); } + protected function serverStatus(): Attribute + { + return Attribute::make( + get: function () { + if ($this->additional_servers->count() === 0) { + return $this->destination->server->isFunctional(); + } else { + $additional_servers_status = $this->additional_servers->pluck('pivot.status'); + $main_server_status = $this->destination->server->isFunctional(); + foreach ($additional_servers_status as $status) { + $server_status = str($status)->before(':')->value(); + if ($server_status !== 'running') { + return false; + } + } + + return $main_server_status; + } + } + ); + } + public function status(): Attribute { return Attribute::make( @@ -407,6 +638,14 @@ class Application extends BaseModel ); } + public function customNginxConfiguration(): Attribute + { + return Attribute::make( + set: fn ($value) => base64_encode($value), + get: fn ($value) => base64_decode($value), + ); + } + public function portsExposesArray(): Attribute { return Attribute::make( @@ -510,6 +749,11 @@ class Application extends BaseModel return $this->hasMany(ApplicationPreview::class); } + public function deployment_queue() + { + return $this->hasMany(ApplicationDeploymentQueue::class); + } + public function destination() { return $this->morphTo(); @@ -532,7 +776,7 @@ class Application extends BaseModel public function get_last_successful_deployment() { - return ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'finished')->where('pull_request_id', 0)->orderBy('created_at', 'desc')->first(); + return ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', ApplicationDeploymentStatus::FINISHED)->where('pull_request_id', 0)->orderBy('created_at', 'desc')->first(); } public function get_last_days_deployments() @@ -632,7 +876,7 @@ class Application extends BaseModel public function isConfigurationChanged(bool $save = false) { - $newConfigHash = $this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect; + $newConfigHash = base64_encode($this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect.$this->custom_nginx_configuration); if ($this->pull_request_id === 0 || $this->pull_request_id === null) { $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort()); } else { @@ -662,21 +906,7 @@ class Application extends BaseModel public function customRepository() { - preg_match('/(?<=:)\d+(?=\/)/', $this->git_repository, $matches); - $port = 22; - if (count($matches) === 1) { - $port = $matches[0]; - $gitHost = str($this->git_repository)->before(':'); - $gitRepo = str($this->git_repository)->after('/'); - $repository = "$gitHost:$gitRepo"; - } else { - $repository = $this->git_repository; - } - - return [ - 'repository' => $repository, - 'port' => $port, - ]; + return convertGitUrl($this->git_repository, $this->deploymentType(), $this->source); } public function generateBaseDir(string $uuid) @@ -684,6 +914,11 @@ class Application extends BaseModel return "/artifacts/{$uuid}"; } + public function dirOnServer() + { + return application_configuration_dir()."/{$this->uuid}"; + } + public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false) { $baseDir = $this->generateBaseDir($deployment_uuid); @@ -838,7 +1073,7 @@ class Application extends BaseModel $source_html_url_host = $url['host']; $source_html_url_scheme = $url['scheme']; - if ($this->source->getMorphClass() == 'App\Models\GithubApp') { + if ($this->source->getMorphClass() === \App\Models\GithubApp::class) { if ($this->source->is_public) { $fullRepoUrl = "{$this->source->html_url}/{$customRepository}"; $git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$customRepository} {$baseDir}"; @@ -921,7 +1156,7 @@ class Application extends BaseModel $commands->push("echo 'Checking out $branch'"); } $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); - } elseif ($git_type === 'github') { + } elseif ($git_type === 'github' || $git_type === 'gitea') { $branch = "pull/{$pull_request_id}/head:$pr_branch_name"; if ($exec_in_docker) { $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")); @@ -965,7 +1200,7 @@ class Application extends BaseModel $commands->push("echo 'Checking out $branch'"); } $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); - } elseif ($git_type === 'github') { + } elseif ($git_type === 'github' || $git_type === 'gitea') { $branch = "pull/{$pull_request_id}/head:$pr_branch_name"; if ($exec_in_docker) { $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")); @@ -997,7 +1232,7 @@ class Application extends BaseModel } } - public function parseRawCompose() + public function oldRawParser() { try { $yaml = Yaml::parse($this->docker_compose_raw); @@ -1005,6 +1240,7 @@ class Application extends BaseModel throw new \Exception($e->getMessage()); } $services = data_get($yaml, 'services'); + $commands = collect([]); $services = collect($services)->map(function ($service) use ($commands) { $serviceVolumes = collect(data_get($service, 'volumes', [])); @@ -1014,9 +1250,9 @@ class Application extends BaseModel $type = null; $source = null; if (is_string($volume)) { - $source = Str::of($volume)->before(':'); + $source = str($volume)->before(':'); if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~')) { - $type = Str::of('bind'); + $type = str('bind'); } } elseif (is_array($volume)) { $type = data_get_str($volume, 'type'); @@ -1057,9 +1293,11 @@ class Application extends BaseModel instant_remote_process($commands, $this->destination->server, false); } - public function parseCompose(int $pull_request_id = 0, ?int $preview_id = null) + public function parse(int $pull_request_id = 0, ?int $preview_id = null) { - if ($this->docker_compose_raw) { + if ((int) $this->compose_parsing_version >= 3) { + return newParser($this, $pull_request_id, $preview_id); + } elseif ($this->docker_compose_raw) { return parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, preview_id: $preview_id); } else { return collect([]); @@ -1072,16 +1310,11 @@ class Application extends BaseModel if ($isInit && $this->docker_compose_raw) { return; } - $uuid = new Cuid2(); + $uuid = new Cuid2; ['commands' => $cloneCommand] = $this->generateGitImportCommands(deployment_uuid: $uuid, only_checkout: true, exec_in_docker: false, custom_base_dir: '.'); $workdir = rtrim($this->base_directory, '/'); $composeFile = $this->docker_compose_location; - // $prComposeFile = $this->docker_compose_pr_location; $fileList = collect([".$workdir$composeFile"]); - // if ($composeFile !== $prComposeFile) { - // $fileList->push(".$prComposeFile"); - // } - $gitRemoteStatus = $this->getGitRemoteStatus(deployment_uuid: $uuid); if (! $gitRemoteStatus['is_accessible']) { throw new \RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}"); @@ -1097,46 +1330,54 @@ class Application extends BaseModel 'git read-tree -mu HEAD', "cat .$workdir$composeFile", ]); - $composeFileContent = instant_remote_process($commands, $this->destination->server, false); - if (! $composeFileContent) { + try { + $composeFileContent = instant_remote_process($commands, $this->destination->server); + } catch (\Exception $e) { + if (str($e->getMessage())->contains('No such file')) { + throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile

Check if you used the right extension (.yaml or .yml) in the compose file name."); + } + if (str($e->getMessage())->contains('fatal: repository') && str($e->getMessage())->contains('does not exist')) { + if ($this->deploymentType() === 'deploy_key') { + throw new \RuntimeException('Your deploy key does not have access to the repository. Please check your deploy key and try again.'); + } + throw new \RuntimeException('Repository does not exist. Please check your repository URL and try again.'); + } + throw new \RuntimeException($e->getMessage()); + } finally { $this->docker_compose_location = $initialDockerComposeLocation; $this->save(); $commands = collect([ "rm -rf /tmp/{$uuid}", ]); instant_remote_process($commands, $this->destination->server, false); - throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile

Check if you used the right extension (.yaml or .yml) in the compose file name."); - } else { + } + if ($composeFileContent) { $this->docker_compose_raw = $composeFileContent; $this->save(); - } - - $commands = collect([ - "rm -rf /tmp/{$uuid}", - ]); - instant_remote_process($commands, $this->destination->server, false); - $parsedServices = $this->parseCompose(); - if ($this->docker_compose_domains) { - $json = collect(json_decode($this->docker_compose_domains)); - $names = collect(data_get($parsedServices, 'services'))->keys()->toArray(); - $jsonNames = $json->keys()->toArray(); - $diff = array_diff($jsonNames, $names); - $json = $json->filter(function ($value, $key) use ($diff) { - return ! in_array($key, $diff); - }); - if ($json) { - $this->docker_compose_domains = json_encode($json); - } else { - $this->docker_compose_domains = null; + $parsedServices = $this->parse(); + if ($this->docker_compose_domains) { + $json = collect(json_decode($this->docker_compose_domains)); + $names = collect(data_get($parsedServices, 'services'))->keys()->toArray(); + $jsonNames = $json->keys()->toArray(); + $diff = array_diff($jsonNames, $names); + $json = $json->filter(function ($value, $key) use ($diff) { + return ! in_array($key, $diff); + }); + if ($json) { + $this->docker_compose_domains = json_encode($json); + } else { + $this->docker_compose_domains = null; + } + $this->save(); } - $this->save(); - } - return [ - 'parsedServices' => $parsedServices, - 'initialDockerComposeLocation' => $this->docker_compose_location, - 'initialDockerComposePrLocation' => $this->docker_compose_pr_location, - ]; + return [ + 'parsedServices' => $parsedServices, + 'initialDockerComposeLocation' => $this->docker_compose_location, + ]; + } else { + throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile

Check if you used the right extension (.yaml or .yml) in the compose file name."); + } } public function parseContainerLabels(?ApplicationPreview $preview = null) @@ -1146,13 +1387,11 @@ class Application extends BaseModel return; } if (base64_encode(base64_decode($customLabels, true)) !== $customLabels) { - ray('custom_labels is not base64 encoded'); $this->custom_labels = str($customLabels)->replace(',', "\n"); $this->custom_labels = base64_encode($customLabels); } $customLabels = base64_decode($this->custom_labels); if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { - ray('custom_labels contains non-ascii characters'); $customLabels = str(implode('|coolify|', generateLabelsApplication($this, $preview)))->replace('|coolify|', "\n"); } $this->custom_labels = base64_encode($customLabels); @@ -1277,7 +1516,7 @@ class Application extends BaseModel $template = $this->preview_url_template; $host = $url->getHost(); $schema = $url->getScheme(); - $random = new Cuid2(7); + $random = new Cuid2; $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $pull_request_id, $preview_fqdn); @@ -1288,4 +1527,126 @@ class Application extends BaseModel return $preview; } + + public static function getDomainsByUuid(string $uuid): array + { + $application = self::where('uuid', $uuid)->first(); + + if ($application) { + return $application->fqdns; + } + + return []; + } + + public function getCpuMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); + + return $parsedCollection->toArray(); + } + } + + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); + } + } + + public function generateConfig($is_json = false) + { + $config = collect([]); + if ($this->build_pack = 'nixpacks') { + $config = collect([ + 'build_pack' => 'nixpacks', + 'docker_registry_image_name' => $this->docker_registry_image_name, + 'docker_registry_image_tag' => $this->docker_registry_image_tag, + 'install_command' => $this->install_command, + 'build_command' => $this->build_command, + 'start_command' => $this->start_command, + 'base_directory' => $this->base_directory, + 'publish_directory' => $this->publish_directory, + 'custom_docker_run_options' => $this->custom_docker_run_options, + 'ports_exposes' => $this->ports_exposes, + 'ports_mappings' => $this->ports_mapping, + 'settings' => collect([ + 'is_static' => $this->settings->is_static, + ]), + ]); + } + $config = $config->filter(function ($value) { + return str($value)->isNotEmpty(); + }); + if ($is_json) { + return json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + } + + return $config; + } + + public function setConfig($config) + { + $validator = Validator::make(['config' => $config], [ + 'config' => 'required|json', + ]); + if ($validator->fails()) { + throw new \Exception('Invalid JSON format'); + } + $config = json_decode($config, true); + + $deepValidator = Validator::make(['config' => $config], [ + 'config.build_pack' => 'required|string', + 'config.base_directory' => 'required|string', + 'config.publish_directory' => 'required|string', + 'config.ports_exposes' => 'required|string', + 'config.settings.is_static' => 'required|boolean', + ]); + if ($deepValidator->fails()) { + throw new \Exception('Invalid data'); + } + $config = $deepValidator->validated()['config']; + + try { + $settings = data_get($config, 'settings', []); + data_forget($config, 'settings'); + $this->update($config); + $this->settings()->update($settings); + } catch (\Exception $e) { + throw new \Exception('Failed to update application settings'); + } + } } diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php index b1c595046..c261c30c6 100644 --- a/app/Models/ApplicationDeploymentQueue.php +++ b/app/Models/ApplicationDeploymentQueue.php @@ -2,13 +2,58 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Carbon; +use OpenApi\Attributes as OA; +#[OA\Schema( + description: 'Project model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer'], + 'application_id' => ['type' => 'string'], + 'deployment_uuid' => ['type' => 'string'], + 'pull_request_id' => ['type' => 'integer'], + 'force_rebuild' => ['type' => 'boolean'], + 'commit' => ['type' => 'string'], + 'status' => ['type' => 'string'], + 'is_webhook' => ['type' => 'boolean'], + 'is_api' => ['type' => 'boolean'], + 'created_at' => ['type' => 'string'], + 'updated_at' => ['type' => 'string'], + 'logs' => ['type' => 'string'], + 'current_process_id' => ['type' => 'string'], + 'restart_only' => ['type' => 'boolean'], + 'git_type' => ['type' => 'string'], + 'server_id' => ['type' => 'integer'], + 'application_name' => ['type' => 'string'], + 'server_name' => ['type' => 'string'], + 'deployment_url' => ['type' => 'string'], + 'destination_id' => ['type' => 'string'], + 'only_this_server' => ['type' => 'boolean'], + 'rollback' => ['type' => 'boolean'], + 'commit_message' => ['type' => 'string'], + ], +)] class ApplicationDeploymentQueue extends Model { protected $guarded = []; + public function application(): Attribute + { + return Attribute::make( + get: fn () => Application::find($this->application_id), + ); + } + + public function server(): Attribute + { + return Attribute::make( + get: fn () => Server::find($this->server_id), + ); + } + public function setStatus(string $status) { $this->update([ diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php index 3bdd24014..bf2bf05bf 100644 --- a/app/Models/ApplicationPreview.php +++ b/app/Models/ApplicationPreview.php @@ -12,9 +12,9 @@ class ApplicationPreview extends BaseModel protected static function booted() { static::deleting(function ($preview) { - if ($preview->application->build_pack === 'dockercompose') { + if (data_get($preview, 'application.build_pack') === 'dockercompose') { $server = $preview->application->destination->server; - $composeFile = $preview->application->parseCompose(pull_request_id: $preview->pull_request_id); + $composeFile = $preview->application->parse(pull_request_id: $preview->pull_request_id); $volumes = data_get($composeFile, 'volumes'); $networks = data_get($composeFile, 'networks'); $networkKeys = collect($networks)->keys(); @@ -28,6 +28,11 @@ class ApplicationPreview extends BaseModel }); } }); + static::saving(function ($preview) { + if ($preview->isDirty('status')) { + $preview->forceFill(['last_online_at' => now()]); + } + }); } public static function findPreviewByApplicationAndPullId(int $application_id, int $pull_request_id) @@ -35,6 +40,11 @@ class ApplicationPreview extends BaseModel return self::where('application_id', $application_id)->where('pull_request_id', $pull_request_id)->firstOrFail(); } + public function isRunning() + { + return (bool) str($this->status)->startsWith('running'); + } + public function application() { return $this->belongsTo(Application::class); @@ -49,7 +59,7 @@ class ApplicationPreview extends BaseModel $template = $this->application->preview_url_template; $host = $url->getHost(); $schema = $url->getScheme(); - $random = new Cuid2(7); + $random = new Cuid2; $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn); diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php index 7e028a6b5..17201ea6e 100644 --- a/app/Models/BaseModel.php +++ b/app/Models/BaseModel.php @@ -14,7 +14,7 @@ abstract class BaseModel extends Model static::creating(function (Model $model) { // Generate a UUID if one isn't set if (! $model->uuid) { - $model->uuid = (string) new Cuid2(7); + $model->uuid = (string) new Cuid2; } }); } diff --git a/app/Models/Environment.php b/app/Models/Environment.php index e84b6989b..71e8bbd21 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -4,7 +4,20 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; +use OpenApi\Attributes as OA; +#[OA\Schema( + description: 'Environment model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer'], + 'name' => ['type' => 'string'], + 'project_id' => ['type' => 'integer'], + 'created_at' => ['type' => 'string'], + 'updated_at' => ['type' => 'string'], + 'description' => ['type' => 'string'], + ] +)] class Environment extends Model { protected $guarded = []; @@ -14,10 +27,8 @@ class Environment extends Model static::deleting(function ($environment) { $shared_variables = $environment->environment_variables(); foreach ($shared_variables as $shared_variable) { - ray('Deleting environment shared variable: '.$shared_variable->name); $shared_variable->delete(); } - }); } @@ -27,6 +38,9 @@ class Environment extends Model $this->redis()->count() == 0 && $this->postgresqls()->count() == 0 && $this->mysqls()->count() == 0 && + $this->keydbs()->count() == 0 && + $this->dragonflies()->count() == 0 && + $this->clickhouses()->count() == 0 && $this->mariadbs()->count() == 0 && $this->mongodbs()->count() == 0 && $this->services()->count() == 0; @@ -109,7 +123,7 @@ class Environment extends Model protected function name(): Attribute { return Attribute::make( - set: fn (string $value) => strtolower($value), + set: fn (string $value) => str($value)->lower()->trim()->replace('/', '-')->toString(), ); } } diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index ff63bca5a..08f23d7ab 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -5,9 +5,32 @@ namespace App\Models; use App\Models\EnvironmentVariable as ModelsEnvironmentVariable; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Str; -use Symfony\Component\Yaml\Yaml; +use OpenApi\Attributes as OA; +use Visus\Cuid2\Cuid2; +#[OA\Schema( + description: 'Environment Variable model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer'], + 'uuid' => ['type' => 'string'], + 'application_id' => ['type' => 'integer'], + 'service_id' => ['type' => 'integer'], + 'database_id' => ['type' => 'integer'], + 'is_build_time' => ['type' => 'boolean'], + 'is_literal' => ['type' => 'boolean'], + 'is_multiline' => ['type' => 'boolean'], + 'is_preview' => ['type' => 'boolean'], + 'is_shared' => ['type' => 'boolean'], + 'is_shown_once' => ['type' => 'boolean'], + 'key' => ['type' => 'string'], + 'value' => ['type' => 'string'], + 'real_value' => ['type' => 'string'], + 'version' => ['type' => 'string'], + 'created_at' => ['type' => 'string'], + 'updated_at' => ['type' => 'string'], + ] +)] class EnvironmentVariable extends Model { protected $guarded = []; @@ -21,10 +44,15 @@ class EnvironmentVariable extends Model 'version' => 'string', ]; - protected $appends = ['real_value', 'is_shared']; + protected $appends = ['real_value', 'is_shared', 'is_really_required']; protected static function booted() { + static::creating(function (Model $model) { + if (! $model->uuid) { + $model->uuid = (string) new Cuid2; + } + }); static::created(function (EnvironmentVariable $environment_variable) { if ($environment_variable->application_id && ! $environment_variable->is_preview) { $found = ModelsEnvironmentVariable::where('key', $environment_variable->key)->where('application_id', $environment_variable->application_id)->where('is_preview', true)->first(); @@ -46,6 +74,9 @@ class EnvironmentVariable extends Model 'version' => config('version'), ]); }); + static::saving(function (EnvironmentVariable $environmentVariable) { + $environmentVariable->updateIsShared(); + }); } public function service() @@ -68,8 +99,22 @@ class EnvironmentVariable extends Model $resource = Application::find($this->application_id); } elseif ($this->service_id) { $resource = Service::find($this->service_id); - } elseif ($this->database_id) { - $resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id')); + } elseif ($this->standalone_postgresql_id) { + $resource = StandalonePostgresql::find($this->standalone_postgresql_id); + } elseif ($this->standalone_redis_id) { + $resource = StandaloneRedis::find($this->standalone_redis_id); + } elseif ($this->standalone_mongodb_id) { + $resource = StandaloneMongodb::find($this->standalone_mongodb_id); + } elseif ($this->standalone_mysql_id) { + $resource = StandaloneMysql::find($this->standalone_mysql_id); + } elseif ($this->standalone_mariadb_id) { + $resource = StandaloneMariadb::find($this->standalone_mariadb_id); + } elseif ($this->standalone_keydb_id) { + $resource = StandaloneKeydb::find($this->standalone_keydb_id); + } elseif ($this->standalone_dragonfly_id) { + $resource = StandaloneDragonfly::find($this->standalone_dragonfly_id); + } elseif ($this->standalone_clickhouse_id) { + $resource = StandaloneClickhouse::find($this->standalone_clickhouse_id); } return $resource; @@ -84,69 +129,14 @@ class EnvironmentVariable extends Model $env = $this->get_real_environment_variables($this->value, $resource); return data_get($env, 'value', $env); - if (is_string($env)) { - return $env; - } - - return $env->value; } ); } - protected function isFoundInCompose(): Attribute + protected function isReallyRequired(): Attribute { return Attribute::make( - get: function () { - if (! $this->application_id) { - return true; - } - $found_in_compose = false; - $found_in_args = false; - $resource = $this->resource(); - $compose = data_get($resource, 'docker_compose_raw'); - if (! $compose) { - return true; - } - $yaml = Yaml::parse($compose); - $services = collect(data_get($yaml, 'services')); - if ($services->isEmpty()) { - return false; - } - foreach ($services as $service) { - $environments = collect(data_get($service, 'environment')); - $args = collect(data_get($service, 'build.args')); - if ($environments->isEmpty() && $args->isEmpty()) { - $found_in_compose = false; - break; - } - - $found_in_compose = $environments->contains(function ($item) { - if (str($item)->contains('=')) { - $item = str($item)->before('='); - } - - return strpos($item, $this->key) !== false; - }); - - if ($found_in_compose) { - break; - } - - $found_in_args = $args->contains(function ($item) { - if (str($item)->contains('=')) { - $item = str($item)->before('='); - } - - return strpos($item, $this->key) !== false; - }); - - if ($found_in_args) { - break; - } - } - - return $found_in_compose || $found_in_args; - } + get: fn () => $this->is_required && str($this->real_value)->isEmpty(), ); } @@ -166,32 +156,38 @@ class EnvironmentVariable extends Model private function get_real_environment_variables(?string $environment_variable = null, $resource = null) { - if ((is_null($environment_variable) && $environment_variable == '') || is_null($resource)) { + if ((is_null($environment_variable) && $environment_variable === '') || is_null($resource)) { return null; } $environment_variable = trim($environment_variable); - $type = str($environment_variable)->after('{{')->before('.')->value; - if (str($environment_variable)->startsWith('{{'.$type) && str($environment_variable)->endsWith('}}')) { - $variable = Str::after($environment_variable, "{$type}."); - $variable = Str::before($variable, '}}'); - $variable = Str::of($variable)->trim()->value; + $sharedEnvsFound = str($environment_variable)->matchAll('/{{(.*?)}}/'); + if ($sharedEnvsFound->isEmpty()) { + return $environment_variable; + } + + foreach ($sharedEnvsFound as $sharedEnv) { + $type = str($sharedEnv)->match('/(.*?)\./'); if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { - return $variable; + continue; } - if ($type === 'environment') { + $variable = str($sharedEnv)->match('/\.(.*)/'); + if ($type->value() === 'environment') { $id = $resource->environment->id; - } elseif ($type === 'project') { + } elseif ($type->value() === 'project') { $id = $resource->environment->project->id; - } else { + } elseif ($type->value() === 'team') { $id = $resource->team()->id; } + if (is_null($id)) { + continue; + } $environment_variable_found = SharedEnvironmentVariable::where('type', $type)->where('key', $variable)->where('team_id', $resource->team()->id)->where("{$type}_id", $id)->first(); if ($environment_variable_found) { - return $environment_variable_found; + $environment_variable = str($environment_variable)->replace("{{{$sharedEnv}}}", $environment_variable_found->value); } } - return $environment_variable; + return str($environment_variable)->value(); } private function get_environment_variables(?string $environment_variable = null): ?string @@ -205,7 +201,7 @@ class EnvironmentVariable extends Model private function set_environment_variables(?string $environment_variable = null): ?string { - if (is_null($environment_variable) && $environment_variable == '') { + if (is_null($environment_variable) && $environment_variable === '') { return null; } $environment_variable = trim($environment_variable); @@ -220,7 +216,14 @@ class EnvironmentVariable extends Model protected function key(): Attribute { return Attribute::make( - set: fn (string $value) => Str::of($value)->trim(), + set: fn (string $value) => str($value)->trim()->replace(' ', '_')->value, ); } + + protected function updateIsShared(): void + { + $type = str($this->value)->after('{{')->before('.')->value; + $isShared = str($this->value)->startsWith('{{'.$type) && str($this->value)->endsWith('}}'); + $this->is_shared = $isShared; + } } diff --git a/app/Models/GithubApp.php b/app/Models/GithubApp.php index daf902daf..0b0e93b12 100644 --- a/app/Models/GithubApp.php +++ b/app/Models/GithubApp.php @@ -20,6 +20,22 @@ class GithubApp extends BaseModel 'webhook_secret', ]; + protected static function booted(): void + { + static::deleting(function (GithubApp $github_app) { + $applications_count = Application::where('source_id', $github_app->id)->count(); + if ($applications_count > 0) { + throw new \Exception('You cannot delete this GitHub App because it is in use by '.$applications_count.' application(s). Delete them first.'); + } + $github_app->privateKey()->delete(); + }); + } + + public static function ownedByCurrentTeam() + { + return GithubApp::whereTeamId(currentTeam()->id); + } + public static function public() { return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get(); @@ -30,15 +46,9 @@ class GithubApp extends BaseModel return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(false)->whereNotNull('app_id')->get(); } - protected static function booted(): void + public function team() { - static::deleting(function (GithubApp $github_app) { - $applications_count = Application::where('source_id', $github_app->id)->count(); - if ($applications_count > 0) { - throw new \Exception('You cannot delete this GitHub App because it is in use by '.$applications_count.' application(s). Delete them first.'); - } - $github_app->privateKey()->delete(); - }); + return $this->belongsTo(Team::class); } public function applications() @@ -55,7 +65,7 @@ class GithubApp extends BaseModel { return Attribute::make( get: function () { - if ($this->getMorphClass() === 'App\Models\GithubApp') { + if ($this->getMorphClass() === \App\Models\GithubApp::class) { return 'github'; } }, diff --git a/app/Models/GitlabApp.php b/app/Models/GitlabApp.php index a789a7e65..2112a4a66 100644 --- a/app/Models/GitlabApp.php +++ b/app/Models/GitlabApp.php @@ -9,6 +9,11 @@ class GitlabApp extends BaseModel 'app_secret', ]; + public static function ownedByCurrentTeam() + { + return GitlabApp::whereTeamId(currentTeam()->id); + } + public function applications() { return $this->morphMany(Application::class, 'source'); diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php index 452c5ca22..eeb803925 100644 --- a/app/Models/InstanceSettings.php +++ b/app/Models/InstanceSettings.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Jobs\PullHelperImageJob; use App\Notifications\Channels\SendsEmail; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; @@ -17,8 +18,26 @@ class InstanceSettings extends Model implements SendsEmail protected $casts = [ 'resale_license' => 'encrypted', 'smtp_password' => 'encrypted', + 'allowed_ip_ranges' => 'array', + 'is_auto_update_enabled' => 'boolean', + 'auto_update_frequency' => 'string', + 'update_check_frequency' => 'string', + 'sentinel_token' => 'encrypted', ]; + protected static function booted(): void + { + static::updated(function ($settings) { + if ($settings->isDirty('helper_version')) { + Server::chunkById(100, function ($servers) { + foreach ($servers as $server) { + PullHelperImageJob::dispatch($server); + } + }); + } + }); + } + public function fqdn(): Attribute { return Attribute::make( @@ -33,6 +52,30 @@ class InstanceSettings extends Model implements SendsEmail ); } + public function updateCheckFrequency(): Attribute + { + return Attribute::make( + set: function ($value) { + return translate_cron_expression($value); + }, + get: function ($value) { + return translate_cron_expression($value); + } + ); + } + + public function autoUpdateFrequency(): Attribute + { + return Attribute::make( + set: function ($value) { + return translate_cron_expression($value); + }, + get: function ($value) { + return translate_cron_expression($value); + } + ); + } + public static function get() { return InstanceSettings::findOrFail(0); @@ -47,4 +90,27 @@ class InstanceSettings extends Model implements SendsEmail return explode(',', $recipients); } + + public function getTitleDisplayName(): string + { + $instanceName = $this->instance_name; + if (! $instanceName) { + return ''; + } + + return "[{$instanceName}]"; + } + + // public function helperVersion(): Attribute + // { + // return Attribute::make( + // get: function ($value) { + // if (isDev()) { + // return 'latest'; + // } + + // return $value; + // } + // ); + // } } diff --git a/app/Models/Kubernetes.php b/app/Models/Kubernetes.php index 2ad7a2110..174cb5bc8 100644 --- a/app/Models/Kubernetes.php +++ b/app/Models/Kubernetes.php @@ -2,6 +2,4 @@ namespace App\Models; -class Kubernetes extends BaseModel -{ -} +class Kubernetes extends BaseModel {} diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php index 62ee4c45c..2c223be77 100644 --- a/app/Models/LocalFileVolume.php +++ b/app/Models/LocalFileVolume.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Events\FileStorageChanged; use Illuminate\Database\Eloquent\Factories\HasFactory; class LocalFileVolume extends BaseModel @@ -23,8 +24,9 @@ class LocalFileVolume extends BaseModel return $this->morphTo('resource'); } - public function deleteStorageOnServer() + public function loadStorageOnServer() { + $this->load(['service']); $isService = data_get($this->resource, 'service'); if ($isService) { $workdir = $this->resource->service->workdir(); @@ -33,20 +35,56 @@ class LocalFileVolume extends BaseModel $workdir = $this->resource->workdir(); $server = $this->resource->destination->server; } - $commands = collect([ - "cd $workdir", - ]); - $fs_path = data_get($this, 'fs_path'); - if ($fs_path && $fs_path != '/' && $fs_path != '.' && $fs_path != '..') { - $commands->push("rm -rf $fs_path"); + $commands = collect([]); + $path = data_get_str($this, 'fs_path'); + if ($path->startsWith('.')) { + $path = $path->after('.'); + $path = $workdir.$path; } - ray($commands); + $isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server); + if ($isFile === 'OK') { + $content = instant_remote_process(["cat $path"], $server, false); + $this->content = $content; + $this->is_directory = false; + $this->save(); + } + } - return instant_remote_process($commands, $server); + public function deleteStorageOnServer() + { + $this->load(['service']); + $isService = data_get($this->resource, 'service'); + if ($isService) { + $workdir = $this->resource->service->workdir(); + $server = $this->resource->service->server; + } else { + $workdir = $this->resource->workdir(); + $server = $this->resource->destination->server; + } + $commands = collect([]); + $path = data_get_str($this, 'fs_path'); + if ($path->startsWith('.')) { + $path = $path->after('.'); + $path = $workdir.$path; + } + $isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server); + $isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server); + if ($path && $path != '/' && $path != '.' && $path != '..') { + if ($isFile === 'OK') { + $commands->push("rm -rf $path > /dev/null 2>&1 || true"); + } elseif ($isDir === 'OK') { + $commands->push("rm -rf $path > /dev/null 2>&1 || true"); + $commands->push("rmdir $path > /dev/null 2>&1 || true"); + } + } + if ($commands->count() > 0) { + return instant_remote_process($commands, $server); + } } public function saveStorageOnServer() { + $this->load(['service']); $isService = data_get($this->resource, 'service'); if ($isService) { $workdir = $this->resource->service->workdir(); @@ -55,13 +93,10 @@ class LocalFileVolume extends BaseModel $workdir = $this->resource->workdir(); $server = $this->resource->destination->server; } - $commands = collect([ - "mkdir -p $workdir > /dev/null 2>&1 || true", - "cd $workdir", - ]); - $is_directory = $this->is_directory; - if ($is_directory) { + $commands = collect([]); + if ($this->is_directory) { $commands->push("mkdir -p $this->fs_path > /dev/null 2>&1 || true"); + $commands->push("cd $workdir"); } if (str($this->fs_path)->startsWith('.') || str($this->fs_path)->startsWith('/') || str($this->fs_path)->startsWith('~')) { $parent_dir = str($this->fs_path)->beforeLast('/'); @@ -69,35 +104,50 @@ class LocalFileVolume extends BaseModel $commands->push("mkdir -p $parent_dir > /dev/null 2>&1 || true"); } } - $fileVolume = $this; - $path = str(data_get($fileVolume, 'fs_path')); - $content = data_get($fileVolume, 'content'); + $path = data_get_str($this, 'fs_path'); + $content = data_get($this, 'content'); if ($path->startsWith('.')) { $path = $path->after('.'); $path = $workdir.$path; } $isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server); $isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server); - if ($isFile == 'OK' && $fileVolume->is_directory) { + if ($isFile === 'OK' && $this->is_directory) { + $content = instant_remote_process(["cat $path"], $server, false); + $this->is_directory = false; + $this->content = $content; + $this->save(); + FileStorageChanged::dispatch(data_get($server, 'team_id')); throw new \Exception('The following file is a file on the server, but you are trying to mark it as a directory. Please delete the file on the server or mark it as directory.'); - } elseif ($isDir == 'OK' && ! $fileVolume->is_directory) { - throw new \Exception('The following file is a directory on the server, but you are trying to mark it as a file.

Please delete the directory on the server or mark it as directory.'); + } elseif ($isDir === 'OK' && ! $this->is_directory) { + if ($path === '/' || $path === '.' || $path === '..' || $path === '' || str($path)->isEmpty() || is_null($path)) { + $this->is_directory = true; + $this->save(); + throw new \Exception('The following file is a directory on the server, but you are trying to mark it as a file.

Please delete the directory on the server or mark it as directory.'); + } + instant_remote_process([ + "rm -fr $path", + "touch $path", + ], $server, false); + FileStorageChanged::dispatch(data_get($server, 'team_id')); } - if (! $fileVolume->is_directory && $isDir == 'NOK') { + if ($isDir === 'NOK' && ! $this->is_directory) { + $chmod = data_get($this, 'chmod'); + $chown = data_get($this, 'chown'); if ($content) { $content = base64_encode($content); - $chmod = $fileVolume->chmod; - $chown = $fileVolume->chown; $commands->push("echo '$content' | base64 -d | tee $path > /dev/null"); - $commands->push("chmod +x $path"); - if ($chown) { - $commands->push("chown $chown $path"); - } - if ($chmod) { - $commands->push("chmod $chmod $path"); - } + } else { + $commands->push("touch $path"); } - } elseif ($isDir == 'NOK' && $fileVolume->is_directory) { + $commands->push("chmod +x $path"); + if ($chown) { + $commands->push("chown $chown $path"); + } + if ($chmod) { + $commands->push("chmod $chmod $path"); + } + } elseif ($isDir === 'NOK' && $this->is_directory) { $commands->push("mkdir -p $path > /dev/null 2>&1 || true"); } diff --git a/app/Models/LocalPersistentVolume.php b/app/Models/LocalPersistentVolume.php index e48b8b405..68e476365 100644 --- a/app/Models/LocalPersistentVolume.php +++ b/app/Models/LocalPersistentVolume.php @@ -4,7 +4,6 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Str; class LocalPersistentVolume extends Model { @@ -33,14 +32,14 @@ class LocalPersistentVolume extends Model protected function name(): Attribute { return Attribute::make( - set: fn (string $value) => Str::of($value)->trim()->value, + set: fn (string $value) => str($value)->trim()->value, ); } protected function mountPath(): Attribute { return Attribute::make( - set: fn (string $value) => Str::of($value)->trim()->start('/')->value + set: fn (string $value) => str($value)->trim()->start('/')->value ); } @@ -49,7 +48,7 @@ class LocalPersistentVolume extends Model return Attribute::make( set: function (?string $value) { if ($value) { - return Str::of($value)->trim()->start('/')->value; + return str($value)->trim()->start('/')->value; } else { return $value; } diff --git a/app/Models/PrivateKey.php b/app/Models/PrivateKey.php index 187dfca58..065746ede 100644 --- a/app/Models/PrivateKey.php +++ b/app/Models/PrivateKey.php @@ -2,41 +2,167 @@ namespace App\Models; +use DanHarrin\LivewireRateLimiting\WithRateLimiting; +use Illuminate\Support\Facades\Storage; +use Illuminate\Validation\ValidationException; +use OpenApi\Attributes as OA; use phpseclib3\Crypt\PublicKeyLoader; +#[OA\Schema( + description: 'Private Key model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer'], + 'uuid' => ['type' => 'string'], + 'name' => ['type' => 'string'], + 'description' => ['type' => 'string'], + 'private_key' => ['type' => 'string', 'format' => 'private-key'], + 'is_git_related' => ['type' => 'boolean'], + 'team_id' => ['type' => 'integer'], + 'created_at' => ['type' => 'string'], + 'updated_at' => ['type' => 'string'], + ], +)] class PrivateKey extends BaseModel { + use WithRateLimiting; + protected $fillable = [ 'name', 'description', 'private_key', 'is_git_related', 'team_id', + 'fingerprint', ]; + protected $casts = [ + 'private_key' => 'encrypted', + ]; + + protected static function booted() + { + static::saving(function ($key) { + $key->private_key = formatPrivateKey($key->private_key); + + if (! self::validatePrivateKey($key->private_key)) { + throw ValidationException::withMessages([ + 'private_key' => ['The private key is invalid.'], + ]); + } + + $key->fingerprint = self::generateFingerprint($key->private_key); + if (self::fingerprintExists($key->fingerprint, $key->id)) { + throw ValidationException::withMessages([ + 'private_key' => ['This private key already exists.'], + ]); + } + }); + + static::deleted(function ($key) { + self::deleteFromStorage($key); + }); + } + + public function getPublicKey() + { + return self::extractPublicKeyFromPrivate($this->private_key) ?? 'Error loading private key'; + } + public static function ownedByCurrentTeam(array $select = ['*']) { $selectArray = collect($select)->concat(['id']); - return PrivateKey::whereTeamId(currentTeam()->id)->select($selectArray->all()); + return self::whereTeamId(currentTeam()->id)->select($selectArray->all()); } - public function publicKey() + public static function validatePrivateKey($privateKey) { try { - return PublicKeyLoader::load($this->private_key)->getPublicKey()->toString('OpenSSH', ['comment' => '']); + PublicKeyLoader::load($privateKey); + + return true; } catch (\Throwable $e) { - return 'Error loading private key'; + return false; } } - public function isEmpty() + public static function createAndStore(array $data) { - if ($this->servers()->count() === 0 && $this->applications()->count() === 0 && $this->githubApps()->count() === 0 && $this->gitlabApps()->count() === 0) { - return true; - } + $privateKey = new self($data); + $privateKey->save(); + $privateKey->storeInFileSystem(); - return false; + return $privateKey; + } + + public static function generateNewKeyPair($type = 'rsa') + { + try { + $instance = new self; + $instance->rateLimit(10); + $name = generate_random_name(); + $description = 'Created by Coolify'; + $keyPair = generateSSHKey($type === 'ed25519' ? 'ed25519' : 'rsa'); + + return [ + 'name' => $name, + 'description' => $description, + 'private_key' => $keyPair['private'], + 'public_key' => $keyPair['public'], + ]; + } catch (\Throwable $e) { + throw new \Exception("Failed to generate new {$type} key: ".$e->getMessage()); + } + } + + public static function extractPublicKeyFromPrivate($privateKey) + { + try { + $key = PublicKeyLoader::load($privateKey); + + return $key->getPublicKey()->toString('OpenSSH', ['comment' => '']); + } catch (\Throwable $e) { + return null; + } + } + + public static function validateAndExtractPublicKey($privateKey) + { + $isValid = self::validatePrivateKey($privateKey); + $publicKey = $isValid ? self::extractPublicKeyFromPrivate($privateKey) : ''; + + return [ + 'isValid' => $isValid, + 'publicKey' => $publicKey, + ]; + } + + public function storeInFileSystem() + { + $filename = "ssh_key@{$this->uuid}"; + Storage::disk('ssh-keys')->put($filename, $this->private_key); + + return "/var/www/html/storage/app/ssh/keys/{$filename}"; + } + + public static function deleteFromStorage(self $privateKey) + { + $filename = "ssh_key@{$privateKey->uuid}"; + Storage::disk('ssh-keys')->delete($filename); + } + + public function getKeyLocation() + { + return "/var/www/html/storage/app/ssh/keys/ssh_key@{$this->uuid}"; + } + + public function updatePrivateKey(array $data) + { + $this->update($data); + $this->storeInFileSystem(); + + return $this; } public function servers() @@ -58,4 +184,53 @@ class PrivateKey extends BaseModel { return $this->hasMany(GitlabApp::class); } + + public function isInUse() + { + return $this->servers()->exists() + || $this->applications()->exists() + || $this->githubApps()->exists() + || $this->gitlabApps()->exists(); + } + + public function safeDelete() + { + if (! $this->isInUse()) { + $this->delete(); + + return true; + } + + return false; + } + + public static function generateFingerprint($privateKey) + { + try { + $key = PublicKeyLoader::load($privateKey); + $publicKey = $key->getPublicKey(); + + return $publicKey->getFingerprint('sha256'); + } catch (\Throwable $e) { + return null; + } + } + + private static function fingerprintExists($fingerprint, $excludeId = null) + { + $query = self::where('fingerprint', $fingerprint); + + if (! is_null($excludeId)) { + $query->where('id', '!=', $excludeId); + } + + return $query->exists(); + } + + public static function cleanupUnusedKeys() + { + self::ownedByCurrentTeam()->each(function ($privateKey) { + $privateKey->safeDelete(); + }); + } } diff --git a/app/Models/Project.php b/app/Models/Project.php index acc98e341..f27e6c208 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -2,13 +2,33 @@ namespace App\Models; +use OpenApi\Attributes as OA; + +#[OA\Schema( + description: 'Project model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer'], + 'uuid' => ['type' => 'string'], + 'name' => ['type' => 'string'], + 'description' => ['type' => 'string'], + 'environments' => new OA\Property( + property: 'environments', + type: 'array', + items: new OA\Items(ref: '#/components/schemas/Environment'), + description: 'The environments of the project.' + ), + ] +)] class Project extends BaseModel { protected $guarded = []; + protected $appends = ['default_environment']; + public static function ownedByCurrentTeam() { - return Project::whereTeamId(currentTeam()->id)->orderBy('name'); + return Project::whereTeamId(currentTeam()->id)->orderByRaw('LOWER(name)'); } protected static function booted() @@ -27,7 +47,6 @@ class Project extends BaseModel $project->settings()->delete(); $shared_variables = $project->environment_variables(); foreach ($shared_variables as $shared_variable) { - ray('Deleting project shared variable: '.$shared_variable->name); $shared_variable->delete(); } }); @@ -103,13 +122,36 @@ class Project extends BaseModel return $this->hasManyThrough(StandaloneMariadb::class, Environment::class); } - public function resource_count() + public function isEmpty() { - return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count() + $this->keydbs()->count() + $this->dragonflies()->count() + $this->services()->count() + $this->clickhouses()->count(); + return $this->applications()->count() == 0 && + $this->redis()->count() == 0 && + $this->postgresqls()->count() == 0 && + $this->mysqls()->count() == 0 && + $this->keydbs()->count() == 0 && + $this->dragonflies()->count() == 0 && + $this->clickhouses()->count() == 0 && + $this->mariadbs()->count() == 0 && + $this->mongodbs()->count() == 0 && + $this->services()->count() == 0; } public function databases() { return $this->postgresqls()->get()->merge($this->redis()->get())->merge($this->mongodbs()->get())->merge($this->mysqls()->get())->merge($this->mariadbs()->get())->merge($this->keydbs()->get())->merge($this->dragonflies()->get())->merge($this->clickhouses()->get()); } + + public function getDefaultEnvironmentAttribute() + { + $default = $this->environments()->where('name', 'production')->first(); + if ($default) { + return $default->name; + } + $default = $this->environments()->get(); + if ($default->count() > 0) { + return $default->sortBy('created_at')->first()->name; + } + + return null; + } } diff --git a/app/Models/S3Storage.php b/app/Models/S3Storage.php index 278ee5995..a432a6e9c 100644 --- a/app/Models/S3Storage.php +++ b/app/Models/S3Storage.php @@ -40,6 +40,16 @@ class S3Storage extends BaseModel return "{$this->endpoint}/{$this->bucket}"; } + public function isHetzner() + { + return str($this->endpoint)->contains('your-objectstorage.com'); + } + + public function isDigitalOcean() + { + return str($this->endpoint)->contains('digitaloceanspaces.com'); + } + public function testConnection(bool $shouldSave = false) { try { @@ -50,7 +60,7 @@ class S3Storage extends BaseModel } catch (\Throwable $e) { $this->is_usable = false; if ($this->unusable_email_sent === false && is_transactional_emails_active()) { - $mail = new MailMessage(); + $mail = new MailMessage; $mail->subject('Coolify: S3 Storage Connection Error'); $mail->view('emails.s3-connection-error', ['name' => $this->name, 'reason' => $e->getMessage(), 'url' => route('storage.show', ['storage_uuid' => $this->uuid])]); $users = collect([]); diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php index edd840e7d..473fc7b4b 100644 --- a/app/Models/ScheduledDatabaseBackup.php +++ b/app/Models/ScheduledDatabaseBackup.php @@ -22,7 +22,8 @@ class ScheduledDatabaseBackup extends BaseModel public function executions(): HasMany { - return $this->hasMany(ScheduledDatabaseBackupExecution::class); + // Last execution first + return $this->hasMany(ScheduledDatabaseBackupExecution::class)->orderBy('created_at', 'desc'); } public function s3() @@ -34,4 +35,22 @@ class ScheduledDatabaseBackup extends BaseModel { return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get(); } + + public function server() + { + if ($this->database) { + if ($this->database instanceof ServiceDatabase) { + $destination = data_get($this->database->service, 'destination'); + $server = data_get($destination, 'server'); + } else { + $destination = data_get($this->database, 'destination'); + $server = data_get($destination, 'server'); + } + if ($server) { + return $server; + } + } + + return null; + } } diff --git a/app/Models/ScheduledTask.php b/app/Models/ScheduledTask.php index 1cb805e8e..264a04d1f 100644 --- a/app/Models/ScheduledTask.php +++ b/app/Models/ScheduledTask.php @@ -26,6 +26,26 @@ class ScheduledTask extends BaseModel public function executions(): HasMany { - return $this->hasMany(ScheduledTaskExecution::class); + // Last execution first + return $this->hasMany(ScheduledTaskExecution::class)->orderBy('created_at', 'desc'); + } + + public function server() + { + if ($this->application) { + if ($this->application->destination && $this->application->destination->server) { + return $this->application->destination->server; + } + } elseif ($this->service) { + if ($this->service->destination && $this->service->destination->server) { + return $this->service->destination->server; + } + } elseif ($this->database) { + if ($this->database->destination && $this->database->destination->server) { + return $this->database->destination->server; + } + } + + return null; } } diff --git a/app/Models/Server.php b/app/Models/Server.php index b1419dc0e..64192c71f 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -3,25 +3,51 @@ namespace App\Models; use App\Actions\Server\InstallDocker; +use App\Actions\Server\StartSentinel; use App\Enums\ProxyTypes; -use App\Jobs\PullSentinelImageJob; -use App\Notifications\Server\Revived; +use App\Jobs\CheckAndStartSentinelJob; +use App\Notifications\Server\Reachable; use App\Notifications\Server\Unreachable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; -use Illuminate\Support\Str; use Illuminate\Support\Stringable; +use OpenApi\Attributes as OA; use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; +#[OA\Schema( + description: 'Server model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer'], + 'uuid' => ['type' => 'string'], + 'name' => ['type' => 'string'], + 'description' => ['type' => 'string'], + 'ip' => ['type' => 'string'], + 'user' => ['type' => 'string'], + 'port' => ['type' => 'integer'], + 'proxy' => ['type' => 'object'], + 'high_disk_usage_notification_sent' => ['type' => 'boolean'], + 'unreachable_notification_sent' => ['type' => 'boolean'], + 'unreachable_count' => ['type' => 'integer'], + 'validation_logs' => ['type' => 'string'], + 'log_drain_notification_sent' => ['type' => 'boolean'], + 'swarm_cluster' => ['type' => 'string'], + 'delete_unused_volumes' => ['type' => 'boolean'], + 'delete_unused_networks' => ['type' => 'boolean'], + ] +)] + class Server extends BaseModel { - use SchemalessAttributesTrait; + use SchemalessAttributesTrait, SoftDeletes; public static $batch_counter = 0; @@ -30,19 +56,56 @@ class Server extends BaseModel static::saving(function ($server) { $payload = []; if ($server->user) { - $payload['user'] = Str::of($server->user)->trim(); + $payload['user'] = str($server->user)->trim(); } if ($server->ip) { - $payload['ip'] = Str::of($server->ip)->trim(); + $payload['ip'] = str($server->ip)->trim(); } $server->forceFill($payload); }); + static::saved(function ($server) { + if ($server->privateKey?->isDirty()) { + refresh_server_connection($server->privateKey); + } + }); static::created(function ($server) { ServerSetting::create([ 'server_id' => $server->id, ]); + if ($server->id === 0) { + if ($server->isSwarm()) { + SwarmDocker::create([ + 'id' => 0, + 'name' => 'coolify', + 'network' => 'coolify-overlay', + 'server_id' => $server->id, + ]); + } else { + StandaloneDocker::create([ + 'id' => 0, + 'name' => 'coolify', + 'network' => 'coolify', + 'server_id' => $server->id, + ]); + } + } else { + if ($server->isSwarm()) { + SwarmDocker::create([ + 'name' => 'coolify-overlay', + 'network' => 'coolify-overlay', + 'server_id' => $server->id, + ]); + } else { + StandaloneDocker::create([ + 'name' => 'coolify', + 'network' => 'coolify', + 'server_id' => $server->id, + ]); + } + } }); - static::deleting(function ($server) { + + static::forceDeleting(function ($server) { $server->destinations()->each(function ($destination) { $destination->delete(); }); @@ -50,18 +113,38 @@ class Server extends BaseModel }); } - public $casts = [ + protected $casts = [ 'proxy' => SchemalessAttributes::class, 'logdrain_axiom_api_key' => 'encrypted', 'logdrain_newrelic_license_key' => 'encrypted', + 'delete_unused_volumes' => 'boolean', + 'delete_unused_networks' => 'boolean', + 'unreachable_notification_sent' => 'boolean', + 'is_build_server' => 'boolean', + 'force_disabled' => 'boolean', ]; protected $schemalessAttributes = [ 'proxy', ]; + protected $fillable = [ + 'name', + 'ip', + 'port', + 'user', + 'description', + 'private_key_id', + 'team_id', + ]; + protected $guarded = []; + public function type() + { + return 'server'; + } + public static function isReachable() { return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true); @@ -94,39 +177,9 @@ class Server extends BaseModel return $this->hasOne(ServerSetting::class); } - public function addInitialNetwork() + public function proxySet() { - if ($this->id === 0) { - if ($this->isSwarm()) { - SwarmDocker::create([ - 'id' => 0, - 'name' => 'coolify', - 'network' => 'coolify-overlay', - 'server_id' => $this->id, - ]); - } else { - StandaloneDocker::create([ - 'id' => 0, - 'name' => 'coolify', - 'network' => 'coolify', - 'server_id' => $this->id, - ]); - } - } else { - if ($this->isSwarm()) { - SwarmDocker::create([ - 'name' => 'coolify-overlay', - 'network' => 'coolify-overlay', - 'server_id' => $this->id, - ]); - } else { - StandaloneDocker::create([ - 'name' => 'coolify', - 'network' => 'coolify', - 'server_id' => $this->id, - ]); - } - } + return $this->proxyType() && $this->proxyType() !== 'NONE' && $this->isFunctional() && ! $this->isSwarmWorker() && ! $this->settings->is_build_server; } public function setupDefault404Redirect() @@ -134,13 +187,13 @@ class Server extends BaseModel $dynamic_conf_path = $this->proxyPath().'/dynamic'; $proxy_type = $this->proxyType(); $redirect_url = $this->proxy->redirect_url; - if ($proxy_type === 'TRAEFIK_V2') { + if ($proxy_type === ProxyTypes::TRAEFIK->value) { $default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml"; - } elseif ($proxy_type === 'CADDY') { + } elseif ($proxy_type === ProxyTypes::CADDY->value) { $default_redirect_file = "$dynamic_conf_path/default_redirect_404.caddy"; } if (empty($redirect_url)) { - if ($proxy_type === 'CADDY') { + if ($proxy_type === ProxyTypes::CADDY->value) { $conf = ':80, :443 { respond 404 }'; @@ -164,7 +217,7 @@ respond 404 return; } - if ($proxy_type === 'TRAEFIK_V2') { + if ($proxy_type === ProxyTypes::TRAEFIK->value) { $dynamic_conf = [ 'http' => [ 'routers' => [ @@ -174,10 +227,13 @@ respond 404 1 => 'https', ], 'service' => 'noop', - 'rule' => 'HostRegexp(`{catchall:.*}`)', + 'rule' => 'HostRegexp(`.+`)', + 'tls' => [ + 'certResolver' => 'letsencrypt', + ], 'priority' => 1, 'middlewares' => [ - 0 => 'redirect-regexp@file', + 0 => 'redirect-regexp', ], ], ], @@ -210,7 +266,7 @@ respond 404 $conf; $base64 = base64_encode($conf); - } elseif ($proxy_type === 'CADDY') { + } elseif ($proxy_type === ProxyTypes::CADDY->value) { $conf = ":80, :443 { redir $redirect_url }"; @@ -226,9 +282,6 @@ respond 404 "echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null", ], $this); - if (config('app.env') == 'local') { - ray($conf); - } if ($proxy_type === 'CADDY') { $this->reloadCaddy(); } @@ -236,11 +289,11 @@ respond 404 public function setupDynamicProxyConfiguration() { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $dynamic_config_path = $this->proxyPath().'/dynamic'; - if ($this->proxyType() === 'TRAEFIK_V2') { + if ($this->proxyType() === ProxyTypes::TRAEFIK->value) { $file = "$dynamic_config_path/coolify.yaml"; - if (empty($settings->fqdn) || (isCloud() && $this->id !== 0)) { + if (empty($settings->fqdn) || (isCloud() && $this->id !== 0) || ! $this->isLocalhost()) { instant_remote_process([ "rm -f $file", ], $this); @@ -278,6 +331,13 @@ respond 404 'service' => 'coolify-realtime', 'rule' => "Host(`{$host}`) && PathPrefix(`/app`)", ], + 'coolify-terminal-ws' => [ + 'entryPoints' => [ + 0 => 'http', + ], + 'service' => 'coolify-terminal', + 'rule' => "Host(`{$host}`) && PathPrefix(`/terminal/ws`)", + ], ], 'services' => [ 'coolify' => [ @@ -298,6 +358,15 @@ respond 404 ], ], ], + 'coolify-terminal' => [ + 'loadBalancer' => [ + 'servers' => [ + 0 => [ + 'url' => 'http://coolify-realtime:6002', + ], + ], + ], + ], ], ], ]; @@ -327,6 +396,16 @@ respond 404 'certresolver' => 'letsencrypt', ], ]; + $traefik_dynamic_conf['http']['routers']['coolify-terminal-wss'] = [ + 'entryPoints' => [ + 0 => 'https', + ], + 'service' => 'coolify-terminal', + 'rule' => "Host(`{$host}`) && PathPrefix(`/terminal/ws`)", + 'tls' => [ + 'certresolver' => 'letsencrypt', + ], + ]; } $yaml = Yaml::dump($traefik_dynamic_conf, 12, 2); $yaml = @@ -340,13 +419,13 @@ respond 404 "echo '$base64' | base64 -d | tee $file > /dev/null", ], $this); - if (config('app.env') == 'local') { + if (config('app.env') === 'local') { // ray($yaml); } } } elseif ($this->proxyType() === 'CADDY') { $file = "$dynamic_config_path/coolify.caddy"; - if (empty($settings->fqdn) || (isCloud() && $this->id !== 0)) { + if (empty($settings->fqdn) || (isCloud() && $this->id !== 0) || ! $this->isLocalhost()) { instant_remote_process([ "rm -f $file", ], $this); @@ -360,6 +439,9 @@ $schema://$host { handle /app/* { reverse_proxy coolify-realtime:6001 } + handle /terminal/ws { + reverse_proxy coolify-realtime:6002 + } reverse_proxy coolify:80 }"; $base64 = base64_encode($caddy_file); @@ -386,12 +468,20 @@ $schema://$host { // TODO: should use /traefik for already exisiting configurations? // Should move everything except /caddy and /nginx to /traefik // The code needs to be modified as well, so maybe it does not worth it - if ($proxyType === ProxyTypes::TRAEFIK_V2->value) { - $proxy_path = $proxy_path; + if ($proxyType === ProxyTypes::TRAEFIK->value) { + // Do nothing } elseif ($proxyType === ProxyTypes::CADDY->value) { - $proxy_path = $proxy_path.'/caddy'; + if (isDev()) { + $proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/caddy'; + } else { + $proxy_path = $proxy_path.'/caddy'; + } } elseif ($proxyType === ProxyTypes::NGINX->value) { - $proxy_path = $proxy_path.'/nginx'; + if (isDev()) { + $proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/nginx'; + } else { + $proxy_path = $proxy_path.'/nginx'; + } } return $proxy_path; @@ -399,15 +489,6 @@ $schema://$host { public function proxyType() { - // $proxyType = $this->proxy->get('type'); - // if ($proxyType === ProxyTypes::NONE->value) { - // return $proxyType; - // } - // if (is_null($proxyType)) { - // $this->proxy->type = ProxyTypes::TRAEFIK_V2->value; - // $this->proxy->status = ProxyStatus::EXITED->value; - // $this->save(); - // } return data_get($this->proxy, 'type'); } @@ -426,20 +507,6 @@ $schema://$host { return Server::whereTeamId($teamId)->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_build_server', true); } - public function skipServer() - { - if ($this->ip === '1.2.3.4') { - // ray('skipping 1.2.3.4'); - return true; - } - if ($this->settings->force_disabled === true) { - // ray('force_disabled'); - return true; - } - - return false; - } - public function isForceDisabled() { return $this->settings->force_disabled; @@ -447,124 +514,112 @@ $schema://$host { public function forceEnableServer() { - $this->settings->update([ - 'force_disabled' => false, - ]); + $this->settings->force_disabled = false; + $this->settings->save(); } public function forceDisableServer() { - $this->settings->update([ - 'force_disabled' => true, - ]); + $this->settings->force_disabled = true; + $this->settings->save(); $sshKeyFileLocation = "id.root@{$this->uuid}"; Storage::disk('ssh-keys')->delete($sshKeyFileLocation); Storage::disk('ssh-mux')->delete($this->muxFilename()); } + public function sentinelHeartbeat(bool $isReset = false) + { + $this->sentinel_updated_at = $isReset ? now()->subMinutes(6000) : now(); + $this->save(); + } + + /** + * Get the wait time for Sentinel to push before performing an SSH check. + * + * @return int The wait time in seconds. + */ + public function waitBeforeDoingSshCheck(): int + { + $wait = $this->settings->sentinel_push_interval_seconds * 3; + if ($wait < 120) { + $wait = 120; + } + + return $wait; + } + + public function isSentinelLive() + { + return Carbon::parse($this->sentinel_updated_at)->isAfter(now()->subSeconds($this->waitBeforeDoingSshCheck())); + } + + public function isSentinelEnabled() + { + return ($this->isMetricsEnabled() || $this->isServerApiEnabled()) && ! $this->isBuildServer(); + } + + public function isMetricsEnabled() + { + return $this->settings->is_metrics_enabled; + } + + public function isServerApiEnabled() + { + return $this->settings->is_sentinel_enabled; + } + public function checkSentinel() { - ray("Checking sentinel on server: {$this->name}"); - if ($this->is_metrics_enabled) { - $sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false); - $sentinel_found = json_decode($sentinel_found, true); - $status = data_get($sentinel_found, '0.State.Status', 'exited'); - if ($status !== 'running') { - ray('Sentinel is not running, starting it...'); - PullSentinelImageJob::dispatch($this); - } else { - ray('Sentinel is running'); + CheckAndStartSentinelJob::dispatch($this); + } + + public function getCpuMetrics(int $mins = 5) + { + if ($this->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false); + if (str($cpu)->contains('error')) { + $error = json_decode($cpu, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); } + $cpu = json_decode($cpu, true); + + return collect($cpu)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); } } - public function getMetrics() + public function getMemoryMetrics(int $mins = 5) { - if ($this->is_metrics_enabled) { - $from = now()->subMinutes(5)->toIso8601ZuluString(); - $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl http://localhost:8888/api/cpu/history?from=$from'"], $this, false); - $cpu = str($cpu)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($cpu)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $value] = explode(',', trim($line)); + if ($this->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false); + if (str($memory)->contains('error')) { + $error = json_decode($memory, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $memory = json_decode($memory, true); + $parsedCollection = collect($memory)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['usedPercent']]; + }); - return [(int) $time, (float) $value]; - }); - })->toArray(); - - return $parsedCollection; + return $parsedCollection->toArray(); } } - public function isServerReady(int $tries = 3) + public function getDiskUsage(): ?string { - if ($this->skipServer()) { - return false; - } - $serverUptimeCheckNumber = $this->unreachable_count; - if ($this->unreachable_count < $tries) { - $serverUptimeCheckNumber = $this->unreachable_count + 1; - } - if ($this->unreachable_count > $tries) { - $serverUptimeCheckNumber = $tries; - } - - $serverUptimeCheckNumberMax = $tries; - - // ray('server: ' . $this->name); - // ray('serverUptimeCheckNumber: ' . $serverUptimeCheckNumber); - // ray('serverUptimeCheckNumberMax: ' . $serverUptimeCheckNumberMax); - - ['uptime' => $uptime] = $this->validateConnection(); - if ($uptime) { - if ($this->unreachable_notification_sent === true) { - $this->update(['unreachable_notification_sent' => false]); - } - - return true; - } else { - if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) { - // Reached max number of retries - if ($this->unreachable_notification_sent === false) { - ray('Server unreachable, sending notification...'); - // $this->team?->notify(new Unreachable($this)); - $this->update(['unreachable_notification_sent' => true]); - } - if ($this->settings->is_reachable === true) { - $this->settings()->update([ - 'is_reachable' => false, - ]); - } - - foreach ($this->applications() as $application) { - $application->update(['status' => 'exited']); - } - foreach ($this->databases() as $database) { - $database->update(['status' => 'exited']); - } - foreach ($this->services()->get() as $service) { - $apps = $service->applications()->get(); - $dbs = $service->databases()->get(); - foreach ($apps as $app) { - $app->update(['status' => 'exited']); - } - foreach ($dbs as $db) { - $db->update(['status' => 'exited']); - } - } - } else { - $this->update([ - 'unreachable_count' => $this->unreachable_count + 1, - ]); - } - - return false; - } - } - - public function getDiskUsage() - { - return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false); + return instant_remote_process(['df / --output=pcent | tr -cd 0-9'], $this, false); + // return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false); } public function definedResources() @@ -591,7 +646,49 @@ $schema://$host { return instant_remote_process(["docker start $id"], $this); } - public function getContainers(): Collection + public function getContainers() + { + $containers = collect([]); + $containerReplicates = collect([]); + if ($this->isSwarm()) { + $containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this, false); + $containers = format_docker_command_output_to_json($containers); + $containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this, false); + if ($containerReplicates) { + $containerReplicates = format_docker_command_output_to_json($containerReplicates); + foreach ($containerReplicates as $containerReplica) { + $name = data_get($containerReplica, 'Name'); + $containers = $containers->map(function ($container) use ($name, $containerReplica) { + if (data_get($container, 'Spec.Name') === $name) { + $replicas = data_get($containerReplica, 'Replicas'); + $running = str($replicas)->explode('/')[0]; + $total = str($replicas)->explode('/')[1]; + if ($running === $total) { + data_set($container, 'State.Status', 'running'); + data_set($container, 'State.Health.Status', 'healthy'); + } else { + data_set($container, 'State.Status', 'starting'); + data_set($container, 'State.Health.Status', 'unhealthy'); + } + } + + return $container; + }); + } + } + } else { + $containers = instant_remote_process(["docker container inspect $(docker container ls -aq) --format '{{json .}}'"], $this, false); + $containers = format_docker_command_output_to_json($containers); + $containerReplicates = collect([]); + } + + return [ + 'containers' => collect($containers) ?? collect([]), + 'containerReplicates' => collect($containerReplicates) ?? collect([]), + ]; + } + + public function getContainersWithSentinel(): Collection { $sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false); $sentinel_found = json_decode($sentinel_found, true); @@ -604,24 +701,21 @@ $schema://$host { $containers = data_get(json_decode($containers, true), 'containers', []); return collect($containers); - } else { - if ($this->isSwarm()) { - $containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this, false); - } else { - $containers = instant_remote_process(['docker container ls -q'], $this, false); - if (! $containers) { - return collect([]); - } - $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this, false); - } - if (is_null($containers)) { - return collect([]); - } - - return format_docker_command_output_to_json($containers); } } + public function loadAllContainers(): Collection + { + if ($this->isFunctional()) { + $containers = instant_remote_process(["docker ps -a --format '{{json .}}'"], $this); + $containers = format_docker_command_output_to_json($containers); + + return collect($containers); + } + + return collect([]); + } + public function loadUnmanagedContainers(): Collection { if ($this->isFunctional()) { @@ -668,9 +762,9 @@ $schema://$host { $clickhouses = data_get($standaloneDocker, 'clickhouses', collect([])); return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses); - })->filter(function ($item) { + })->flatten()->filter(function ($item) { return data_get($item, 'name') !== 'coolify-db'; - })->flatten(); + }); } public function applications() @@ -714,6 +808,33 @@ $schema://$host { return $this->hasMany(Service::class); } + public function port(): Attribute + { + return Attribute::make( + get: function ($value) { + return preg_replace('/[^0-9]/', '', $value); + } + ); + } + + public function user(): Attribute + { + return Attribute::make( + get: function ($value) { + return preg_replace('/[^A-Za-z0-9\-_]/', '', $value); + } + ); + } + + public function ip(): Attribute + { + return Attribute::make( + get: function ($value) { + return preg_replace('/[^0-9a-zA-Z.:%-]/', '', $value); + } + ); + } + public function getIp(): Attribute { return Attribute::make( @@ -744,8 +865,6 @@ $schema://$host { $standalone_docker = $this->hasMany(StandaloneDocker::class)->get(); $swarm_docker = $this->hasMany(SwarmDocker::class)->get(); - // $additional_dockers = $this->belongsToMany(StandaloneDocker::class, 'additional_destinations')->withPivot('server_id')->get(); - // return $standalone_docker->concat($swarm_docker)->concat($additional_dockers); return $standalone_docker->concat($swarm_docker); } @@ -766,7 +885,7 @@ $schema://$host { public function muxFilename() { - return "{$this->ip}_{$this->port}_{$this->user}"; + return $this->uuid; } public function team() @@ -776,20 +895,32 @@ $schema://$host { public function isProxyShouldRun() { - if ($this->proxyType() === ProxyTypes::NONE->value || $this->settings->is_build_server) { + // TODO: Do we need "|| $this->proxy->force_stop" here? + if ($this->proxyType() === ProxyTypes::NONE->value || $this->isBuildServer()) { return false; } return true; } + public function skipServer() + { + if ($this->ip === '1.2.3.4') { + return true; + } + if ($this->settings->force_disabled === true) { + return true; + } + + return false; + } + public function isFunctional() { - $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && ! $this->settings->force_disabled; - ['private_key_filename' => $private_key_filename, 'mux_filename' => $mux_filename] = server_ssh_configuration($this); - if (! $isFunctional) { - Storage::disk('ssh-keys')->delete($private_key_filename); - Storage::disk('ssh-mux')->delete($mux_filename); + $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && $this->settings->force_disabled === false && $this->ip !== '1.2.3.4'; + + if ($isFunctional === false) { + Storage::disk('ssh-mux')->delete($this->muxFilename()); } return $isFunctional; @@ -806,7 +937,7 @@ $schema://$host { $releaseLines = collect(explode("\n", $os_release)); $collectedData = collect([]); foreach ($releaseLines as $line) { - $item = Str::of($line)->trim(); + $item = str($line)->trim(); $collectedData->put($item->before('=')->value(), $item->after('=')->lower()->replace('"', '')->value()); } $ID = data_get($collectedData, 'ID'); @@ -841,36 +972,110 @@ $schema://$host { return data_get($this, 'settings.is_swarm_worker'); } - public function validateConnection() + public function serverStatus(): bool { - config()->set('coolify.mux_enabled', false); - - $server = Server::find($this->id); - if (! $server) { - return ['uptime' => false, 'error' => 'Server not found.']; + if ($this->status() === false) { + return false; } - if ($server->skipServer()) { + if ($this->isFunctional() === false) { + return false; + } + + return true; + } + + public function status(): bool + { + ['uptime' => $uptime] = $this->validateConnection(false); + if ($uptime === false) { + foreach ($this->applications() as $application) { + $application->status = 'exited'; + $application->save(); + } + foreach ($this->databases() as $database) { + $database->status = 'exited'; + $database->save(); + } + foreach ($this->services() as $service) { + $apps = $service->applications()->get(); + $dbs = $service->databases()->get(); + foreach ($apps as $app) { + $app->status = 'exited'; + $app->save(); + } + foreach ($dbs as $db) { + $db->status = 'exited'; + $db->save(); + } + } + + return false; + } + + return true; + } + + public function isReachableChanged() + { + $this->refresh(); + $unreachableNotificationSent = (bool) $this->unreachable_notification_sent; + $isReachable = (bool) $this->settings->is_reachable; + // If the server is reachable, send the reachable notification if it was sent before + if ($isReachable === true) { + if ($unreachableNotificationSent === true) { + $this->sendReachableNotification(); + } + } else { + // If the server is unreachable, send the unreachable notification if it was not sent before + if ($unreachableNotificationSent === false) { + $this->sendUnreachableNotification(); + } + } + } + + public function sendReachableNotification() + { + $this->unreachable_notification_sent = false; + $this->save(); + $this->refresh(); + $this->team->notify(new Reachable($this)); + } + + public function sendUnreachableNotification() + { + $this->unreachable_notification_sent = true; + $this->save(); + $this->refresh(); + $this->team->notify(new Unreachable($this)); + } + + public function validateConnection(bool $isManualCheck = true, bool $justCheckingNewKey = false) + { + config()->set('constants.ssh.mux_enabled', ! $isManualCheck); + + if ($this->skipServer()) { return ['uptime' => false, 'error' => 'Server skipped.']; } try { - // EC2 does not have `uptime` command, lol - instant_remote_process(['ls /'], $server); - $server->settings()->update([ - 'is_reachable' => true, - ]); - $server->update([ - 'unreachable_count' => 0, - ]); - if (data_get($server, 'unreachable_notification_sent') === true) { - // $server->team?->notify(new Revived($server)); - $server->update(['unreachable_notification_sent' => false]); + // Make sure the private key is stored + if ($this->privateKey) { + $this->privateKey->storeInFileSystem(); + } + instant_remote_process(['ls /'], $this); + if ($this->settings->is_reachable === false) { + $this->settings->is_reachable = true; + $this->settings->save(); } return ['uptime' => true, 'error' => null]; } catch (\Throwable $e) { - $server->settings()->update([ - 'is_reachable' => false, - ]); + if ($justCheckingNewKey) { + return ['uptime' => false, 'error' => 'This key is not valid for this server.']; + } + if ($this->settings->is_reachable === true) { + $this->settings->is_reachable = false; + $this->settings->save(); + } return ['uptime' => false, 'error' => $e->getMessage()]; } @@ -878,9 +1083,7 @@ $schema://$host { public function installDocker() { - $activity = InstallDocker::run($this); - - return $activity; + return InstallDocker::run($this); } public function validateDockerEngine($throwError = false) @@ -991,4 +1194,61 @@ $schema://$host { { return $this->settings->is_build_server; } + + public static function createWithPrivateKey(array $data, PrivateKey $privateKey) + { + $server = new self($data); + $server->privateKey()->associate($privateKey); + $server->save(); + + return $server; + } + + public function updateWithPrivateKey(array $data, ?PrivateKey $privateKey = null) + { + $this->update($data); + if ($privateKey) { + $this->privateKey()->associate($privateKey); + $this->save(); + } + + return $this; + } + + public function storageCheck(): ?string + { + $commands = [ + 'df / --output=pcent | tr -cd 0-9', + ]; + + return instant_remote_process($commands, $this, false); + } + + public function isIpv6(): bool + { + return str($this->ip)->contains(':'); + } + + public function restartSentinel(bool $async = true) + { + try { + if ($async) { + StartSentinel::dispatch($this, true); + } else { + StartSentinel::run($this, true); + } + } catch (\Throwable $e) { + return handleError($e); + } + } + + public function url() + { + return base_url().'/server/'.$this->uuid; + } + + public function restartContainer(string $containerName) + { + return instant_remote_process(['docker restart '.$containerName], $this, false); + } } diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index 9235848ee..fc2c5a0f4 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -2,14 +2,151 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Log; +use OpenApi\Attributes as OA; +#[OA\Schema( + description: 'Server Settings model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer'], + 'concurrent_builds' => ['type' => 'integer'], + 'dynamic_timeout' => ['type' => 'integer'], + 'force_disabled' => ['type' => 'boolean'], + 'force_server_cleanup' => ['type' => 'boolean'], + 'is_build_server' => ['type' => 'boolean'], + 'is_cloudflare_tunnel' => ['type' => 'boolean'], + 'is_jump_server' => ['type' => 'boolean'], + 'is_logdrain_axiom_enabled' => ['type' => 'boolean'], + 'is_logdrain_custom_enabled' => ['type' => 'boolean'], + 'is_logdrain_highlight_enabled' => ['type' => 'boolean'], + 'is_logdrain_newrelic_enabled' => ['type' => 'boolean'], + 'is_metrics_enabled' => ['type' => 'boolean'], + 'is_reachable' => ['type' => 'boolean'], + 'is_sentinel_enabled' => ['type' => 'boolean'], + 'is_swarm_manager' => ['type' => 'boolean'], + 'is_swarm_worker' => ['type' => 'boolean'], + 'is_usable' => ['type' => 'boolean'], + 'logdrain_axiom_api_key' => ['type' => 'string'], + 'logdrain_axiom_dataset_name' => ['type' => 'string'], + 'logdrain_custom_config' => ['type' => 'string'], + 'logdrain_custom_config_parser' => ['type' => 'string'], + 'logdrain_highlight_project_id' => ['type' => 'string'], + 'logdrain_newrelic_base_uri' => ['type' => 'string'], + 'logdrain_newrelic_license_key' => ['type' => 'string'], + 'sentinel_metrics_history_days' => ['type' => 'integer'], + 'sentinel_metrics_refresh_rate_seconds' => ['type' => 'integer'], + 'sentinel_token' => ['type' => 'string'], + 'docker_cleanup_frequency' => ['type' => 'string'], + 'docker_cleanup_threshold' => ['type' => 'integer'], + 'server_id' => ['type' => 'integer'], + 'wildcard_domain' => ['type' => 'string'], + 'created_at' => ['type' => 'string'], + 'updated_at' => ['type' => 'string'], + ] +)] class ServerSetting extends Model { protected $guarded = []; + protected $casts = [ + 'force_docker_cleanup' => 'boolean', + 'docker_cleanup_threshold' => 'integer', + 'sentinel_token' => 'encrypted', + 'is_reachable' => 'boolean', + 'is_usable' => 'boolean', + ]; + + protected static function booted() + { + static::creating(function ($setting) { + try { + if (str($setting->sentinel_token)->isEmpty()) { + $setting->generateSentinelToken(save: false, ignoreEvent: true); + } + if (str($setting->sentinel_custom_url)->isEmpty()) { + $setting->generateSentinelUrl(save: false, ignoreEvent: true); + } + } catch (\Throwable $e) { + Log::error('Error creating server setting: '.$e->getMessage()); + } + }); + static::updated(function ($settings) { + if ( + $settings->isDirty('sentinel_token') || + $settings->isDirty('sentinel_custom_url') || + $settings->isDirty('sentinel_metrics_refresh_rate_seconds') || + $settings->isDirty('sentinel_metrics_history_days') || + $settings->isDirty('sentinel_push_interval_seconds') + ) { + $settings->server->restartSentinel(); + } + if ($settings->isDirty('is_reachable')) { + $settings->server->isReachableChanged(); + } + }); + } + + public function generateSentinelToken(bool $save = true, bool $ignoreEvent = false) + { + $data = [ + 'server_uuid' => $this->server->uuid, + ]; + $token = json_encode($data); + $encrypted = encrypt($token); + $this->sentinel_token = $encrypted; + if ($save) { + if ($ignoreEvent) { + $this->saveQuietly(); + } else { + $this->save(); + } + } + + return $token; + } + + public function generateSentinelUrl(bool $save = true, bool $ignoreEvent = false) + { + $domain = null; + $settings = InstanceSettings::get(); + if ($this->server->isLocalhost()) { + $domain = 'http://host.docker.internal:8000'; + } elseif ($settings->fqdn) { + $domain = $settings->fqdn; + } elseif ($settings->public_ipv4) { + $domain = 'http://'.$settings->public_ipv4.':8000'; + } elseif ($settings->public_ipv6) { + $domain = 'http://'.$settings->public_ipv6.':8000'; + } + $this->sentinel_custom_url = $domain; + if ($save) { + if ($ignoreEvent) { + $this->saveQuietly(); + } else { + $this->save(); + } + } + + return $domain; + } + public function server() { return $this->belongsTo(Server::class); } + + public function dockerCleanupFrequency(): Attribute + { + return Attribute::make( + set: function ($value) { + return translate_cron_expression($value); + }, + get: function ($value) { + return translate_cron_expression($value); + } + ); + } } diff --git a/app/Models/Service.php b/app/Models/Service.php index 7851eb58a..0c9e081a1 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -2,17 +2,60 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Process\InvokedProcess; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Process; +use Illuminate\Support\Facades\Storage; +use OpenApi\Attributes as OA; +use Spatie\Url\Url; +use Visus\Cuid2\Cuid2; +#[OA\Schema( + description: 'Service model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer', 'description' => 'The unique identifier of the service. Only used for database identification.'], + 'uuid' => ['type' => 'string', 'description' => 'The unique identifier of the service.'], + 'name' => ['type' => 'string', 'description' => 'The name of the service.'], + 'environment_id' => ['type' => 'integer', 'description' => 'The unique identifier of the environment where the service is attached to.'], + 'server_id' => ['type' => 'integer', 'description' => 'The unique identifier of the server where the service is running.'], + 'description' => ['type' => 'string', 'description' => 'The description of the service.'], + 'docker_compose_raw' => ['type' => 'string', 'description' => 'The raw docker-compose.yml file of the service.'], + 'docker_compose' => ['type' => 'string', 'description' => 'The docker-compose.yml file that is parsed and modified by Coolify.'], + 'destination_type' => ['type' => 'string', 'description' => 'Destination type.'], + 'destination_id' => ['type' => 'integer', 'description' => 'The unique identifier of the destination where the service is running.'], + 'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'], + 'is_container_label_escape_enabled' => ['type' => 'boolean', 'description' => 'The flag to enable the container label escape.'], + 'is_container_label_readonly_enabled' => ['type' => 'boolean', 'description' => 'The flag to enable the container label readonly.'], + 'config_hash' => ['type' => 'string', 'description' => 'The hash of the service configuration.'], + 'service_type' => ['type' => 'string', 'description' => 'The type of the service.'], + 'created_at' => ['type' => 'string', 'description' => 'The date and time when the service was created.'], + 'updated_at' => ['type' => 'string', 'description' => 'The date and time when the service was last updated.'], + 'deleted_at' => ['type' => 'string', 'description' => 'The date and time when the service was deleted.'], + ], +)] class Service extends BaseModel { use HasFactory, SoftDeletes; + private static $parserVersion = '4'; + protected $guarded = []; + protected $appends = ['server_status']; + + protected static function booted() + { + static::created(function ($service) { + $service->compose_parsing_version = self::$parserVersion; + $service->save(); + }); + } + public function isConfigurationChanged(bool $save = false) { $domains = $this->applications()->get()->pluck('fqdn')->sort()->toArray(); @@ -51,6 +94,20 @@ class Service extends BaseModel } } + protected function serverStatus(): Attribute + { + return Attribute::make( + get: function () { + return $this->server->isFunctional(); + } + ); + } + + public function isRunning() + { + return (bool) str($this->status())->contains('running'); + } + public function isExited() { return (bool) str($this->status())->contains('exited'); @@ -76,15 +133,86 @@ class Service extends BaseModel return $this->morphToMany(Tag::class, 'taggable'); } + public static function ownedByCurrentTeam() + { + return Service::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + + public function getContainersToStop(): array + { + $containersToStop = []; + $applications = $this->applications()->get(); + foreach ($applications as $application) { + $containersToStop[] = "{$application->name}-{$this->uuid}"; + } + $dbs = $this->databases()->get(); + foreach ($dbs as $db) { + $containersToStop[] = "{$db->name}-{$this->uuid}"; + } + + return $containersToStop; + } + + public function stopContainers(array $containerNames, $server, int $timeout = 300) + { + $processes = []; + foreach ($containerNames as $containerName) { + $processes[$containerName] = $this->stopContainer($containerName, $timeout); + } + + $startTime = time(); + while (count($processes) > 0) { + $finishedProcesses = array_filter($processes, function ($process) { + return ! $process->running(); + }); + foreach (array_keys($finishedProcesses) as $containerName) { + unset($processes[$containerName]); + $this->removeContainer($containerName, $server); + } + + if (time() - $startTime >= $timeout) { + $this->forceStopRemainingContainers(array_keys($processes), $server); + break; + } + + usleep(100000); + } + } + + public function stopContainer(string $containerName, int $timeout): InvokedProcess + { + return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); + } + + public function removeContainer(string $containerName, $server) + { + instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false); + } + + public function forceStopRemainingContainers(array $containerNames, $server) + { + foreach ($containerNames as $containerName) { + instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false); + $this->removeContainer($containerName, $server); + } + } + public function delete_configurations() { - $server = data_get($this, 'server'); + $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); if (str($workdir)->endsWith($this->uuid)) { instant_remote_process(['rm -rf '.$this->workdir()], $server, false); } } + public function delete_connected_networks($uuid) + { + $server = data_get($this, 'destination.server'); + instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false); + instant_remote_process(["docker network rm {$uuid}"], $server, false); + } + public function status() { $applications = $this->applications; @@ -160,9 +288,196 @@ class Service extends BaseModel $fields = collect([]); $applications = $this->applications()->get(); foreach ($applications as $application) { - $image = str($application->image)->before(':')->value(); + $image = str($application->image)->before(':'); + if ($image->isEmpty()) { + continue; + } switch ($image) { - case str($image)?->contains('tolgee'): + case $image->contains('castopod'): + $data = collect([]); + $disable_https = $this->environment_variables()->where('key', 'CP_DISABLE_HTTPS')->first(); + if ($disable_https) { + $data = $data->merge([ + 'Disable HTTPS' => [ + 'key' => 'CP_DISABLE_HTTPS', + 'value' => data_get($disable_https, 'value'), + 'rules' => 'required', + 'customHelper' => 'If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS', + ], + ]); + } + $fields->put('Castopod', $data->toArray()); + break; + case $image->contains('label-studio'): + $data = collect([]); + $username = $this->environment_variables()->where('key', 'LABEL_STUDIO_USERNAME')->first(); + $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_LABELSTUDIO')->first(); + if ($username) { + $data = $data->merge([ + 'Username' => [ + 'key' => 'LABEL_STUDIO_USERNAME', + 'value' => data_get($username, 'value'), + 'rules' => 'required', + ], + ]); + } + if ($password) { + $data = $data->merge([ + 'Password' => [ + 'key' => data_get($password, 'key'), + 'value' => data_get($password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + $fields->put('Label Studio', $data->toArray()); + break; + case $image->contains('litellm'): + $data = collect([]); + $username = $this->environment_variables()->where('key', 'SERVICE_USER_UI')->first(); + $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_UI')->first(); + if ($username) { + $data = $data->merge([ + 'Username' => [ + 'key' => data_get($username, 'key'), + 'value' => data_get($username, 'value'), + 'rules' => 'required', + ], + ]); + } + if ($password) { + $data = $data->merge([ + 'Password' => [ + 'key' => data_get($password, 'key'), + 'value' => data_get($password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + $fields->put('Litellm', $data->toArray()); + break; + case $image->contains('langfuse'): + $data = collect([]); + $email = $this->environment_variables()->where('key', 'LANGFUSE_INIT_USER_EMAIL')->first(); + if ($email) { + $data = $data->merge([ + 'Admin Email' => [ + 'key' => data_get($email, 'key'), + 'value' => data_get($email, 'value'), + 'rules' => 'required|email', + ], + ]); + } + $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_LANGFUSE')->first(); + if ($password) { + $data = $data->merge([ + 'Admin Password' => [ + 'key' => data_get($password, 'key'), + 'value' => data_get($password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + $fields->put('Langfuse', $data->toArray()); + break; + case $image->contains('invoiceninja'): + $data = collect([]); + $email = $this->environment_variables()->where('key', 'IN_USER_EMAIL')->first(); + $data = $data->merge([ + 'Email' => [ + 'key' => data_get($email, 'key'), + 'value' => data_get($email, 'value'), + 'rules' => 'required|email', + ], + ]); + $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_INVOICENINJAUSER')->first(); + $data = $data->merge([ + 'Password' => [ + 'key' => data_get($password, 'key'), + 'value' => data_get($password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + $fields->put('Invoice Ninja', $data->toArray()); + break; + case $image->contains('argilla'): + $data = collect([]); + $api_key = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_APIKEY')->first(); + $data = $data->merge([ + 'API Key' => [ + 'key' => data_get($api_key, 'key'), + 'value' => data_get($api_key, 'value'), + 'isPassword' => true, + 'rules' => 'required', + ], + ]); + $data = $data->merge([ + 'API Key' => [ + 'key' => data_get($api_key, 'key'), + 'value' => data_get($api_key, 'value'), + 'isPassword' => true, + 'rules' => 'required', + ], + ]); + $username = $this->environment_variables()->where('key', 'ARGILLA_USERNAME')->first(); + $data = $data->merge([ + 'Username' => [ + 'key' => data_get($username, 'key'), + 'value' => data_get($username, 'value'), + 'rules' => 'required', + ], + ]); + $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ARGILLA')->first(); + $data = $data->merge([ + 'Password' => [ + 'key' => data_get($password, 'key'), + 'value' => data_get($password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + $fields->put('Argilla', $data->toArray()); + break; + case $image->contains('rabbitmq'): + $data = collect([]); + $host_port = $this->environment_variables()->where('key', 'PORT')->first(); + $username = $this->environment_variables()->where('key', 'SERVICE_USER_RABBITMQ')->first(); + $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_RABBITMQ')->first(); + if ($host_port) { + $data = $data->merge([ + 'Host Port Binding' => [ + 'key' => data_get($host_port, 'key'), + 'value' => data_get($host_port, 'value'), + 'rules' => 'required', + ], + ]); + } + if ($username) { + $data = $data->merge([ + 'Username' => [ + 'key' => data_get($username, 'key'), + 'value' => data_get($username, 'value'), + 'rules' => 'required', + ], + ]); + } + if ($password) { + $data = $data->merge([ + 'Password' => [ + 'key' => data_get($password, 'key'), + 'value' => data_get($password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + $fields->put('RabbitMQ', $data->toArray()); + break; + case $image->contains('tolgee'): $data = collect([]); $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_TOLGEE')->first(); $data = $data->merge([ @@ -176,7 +491,7 @@ class Service extends BaseModel if ($admin_password) { $data = $data->merge([ 'Admin Password' => [ - 'key' => 'SERVICE_PASSWORD_TOLGEE', + 'key' => data_get($admin_password, 'key'), 'value' => data_get($admin_password, 'value'), 'rules' => 'required', 'isPassword' => true, @@ -185,7 +500,7 @@ class Service extends BaseModel } $fields->put('Tolgee', $data->toArray()); break; - case str($image)?->contains('logto'): + case $image->contains('logto'): $data = collect([]); $logto_endpoint = $this->environment_variables()->where('key', 'LOGTO_ENDPOINT')->first(); $logto_admin_endpoint = $this->environment_variables()->where('key', 'LOGTO_ADMIN_ENDPOINT')->first(); @@ -209,7 +524,7 @@ class Service extends BaseModel } $fields->put('Logto', $data->toArray()); break; - case str($image)?->contains('unleash-server'): + case $image->contains('unleash-server'): $data = collect([]); $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_UNLEASH')->first(); $data = $data->merge([ @@ -223,7 +538,7 @@ class Service extends BaseModel if ($admin_password) { $data = $data->merge([ 'Admin Password' => [ - 'key' => 'SERVICE_PASSWORD_UNLEASH', + 'key' => data_get($admin_password, 'key'), 'value' => data_get($admin_password, 'value'), 'rules' => 'required', 'isPassword' => true, @@ -232,7 +547,7 @@ class Service extends BaseModel } $fields->put('Unleash', $data->toArray()); break; - case str($image)?->contains('grafana'): + case $image->contains('grafana'): $data = collect([]); $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GRAFANA')->first(); $data = $data->merge([ @@ -246,7 +561,7 @@ class Service extends BaseModel if ($admin_password) { $data = $data->merge([ 'Admin Password' => [ - 'key' => 'GF_SECURITY_ADMIN_PASSWORD', + 'key' => data_get($admin_password, 'key'), 'value' => data_get($admin_password, 'value'), 'rules' => 'required', 'isPassword' => true, @@ -255,7 +570,7 @@ class Service extends BaseModel } $fields->put('Grafana', $data->toArray()); break; - case str($image)?->contains('directus'): + case $image->contains('directus'): $data = collect([]); $admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first(); $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first(); @@ -281,7 +596,7 @@ class Service extends BaseModel } $fields->put('Directus', $data->toArray()); break; - case str($image)?->contains('kong'): + case $image->contains('kong'): $data = collect([]); $dashboard_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first(); $dashboard_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first(); @@ -305,7 +620,7 @@ class Service extends BaseModel ]); } $fields->put('Supabase', $data->toArray()); - case str($image)?->contains('minio'): + case $image->contains('minio'): $data = collect([]); $console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first(); $s3_api_url = $this->environment_variables()->where('key', 'MINIO_SERVER_URL')->first(); @@ -358,7 +673,7 @@ class Service extends BaseModel $fields->put('MinIO', $data->toArray()); break; - case str($image)?->contains('weblate'): + case $image->contains('weblate'): $data = collect([]); $admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first(); $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_WEBLATE')->first(); @@ -384,7 +699,7 @@ class Service extends BaseModel } $fields->put('Weblate', $data->toArray()); break; - case str($image)?->contains('meilisearch'): + case $image->contains('meilisearch'): $data = collect([]); $SERVICE_PASSWORD_MEILISEARCH = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MEILISEARCH')->first(); if ($SERVICE_PASSWORD_MEILISEARCH) { @@ -398,7 +713,7 @@ class Service extends BaseModel } $fields->put('Meilisearch', $data->toArray()); break; - case str($image)?->contains('ghost'): + case $image->contains('ghost'): $data = collect([]); $MAIL_OPTIONS_AUTH_PASS = $this->environment_variables()->where('key', 'MAIL_OPTIONS_AUTH_PASS')->first(); $MAIL_OPTIONS_AUTH_USER = $this->environment_variables()->where('key', 'MAIL_OPTIONS_AUTH_USER')->first(); @@ -458,33 +773,8 @@ class Service extends BaseModel $fields->put('Ghost', $data->toArray()); break; - default: - $data = collect([]); - $admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first(); - $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first(); - if ($admin_user) { - $data = $data->merge([ - 'User' => [ - 'key' => 'SERVICE_USER_ADMIN', - 'value' => data_get($admin_user, 'value', 'admin'), - 'readonly' => true, - 'rules' => 'required', - ], - ]); - } - if ($admin_password) { - $data = $data->merge([ - 'Password' => [ - 'key' => 'SERVICE_PASSWORD_ADMIN', - 'value' => data_get($admin_password, 'value'), - 'rules' => 'required', - 'isPassword' => true, - ], - ]); - } - $fields->put('Admin', $data->toArray()); - break; - case str($image)?->contains('vaultwarden'): + + case $image->contains('vaultwarden'): $data = collect([]); $DATABASE_URL = $this->environment_variables()->where('key', 'DATABASE_URL')->first(); @@ -550,14 +840,128 @@ class Service extends BaseModel $fields->put('Vaultwarden', $data); break; + case $image->contains('gitlab/gitlab'): + $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GITLAB')->first(); + $data = collect([]); + if ($password) { + $data = $data->merge([ + 'Root Password' => [ + 'key' => data_get($password, 'key'), + 'value' => data_get($password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + $data = $data->merge([ + 'Root User' => [ + 'key' => 'GITLAB_ROOT_USER', + 'value' => 'root', + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + + $fields->put('GitLab', $data->toArray()); + break; + case $image->contains('code-server'): + $data = collect([]); + $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_64_PASSWORDCODESERVER')->first(); + if ($password) { + $data = $data->merge([ + 'Password' => [ + 'key' => data_get($password, 'key'), + 'value' => data_get($password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + $sudoPassword = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_SUDOCODESERVER')->first(); + if ($sudoPassword) { + $data = $data->merge([ + 'Sudo Password' => [ + 'key' => data_get($sudoPassword, 'key'), + 'value' => data_get($sudoPassword, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + $fields->put('Code Server', $data->toArray()); + break; + case $image->contains('elestio/strapi'): + $data = collect([]); + $license = $this->environment_variables()->where('key', 'STRAPI_LICENSE')->first(); + if ($license) { + $data = $data->merge([ + 'License' => [ + 'key' => data_get($license, 'key'), + 'value' => data_get($license, 'value'), + ], + ]); + } + $nodeEnv = $this->environment_variables()->where('key', 'NODE_ENV')->first(); + if ($nodeEnv) { + $data = $data->merge([ + 'Node Environment' => [ + 'key' => data_get($nodeEnv, 'key'), + 'value' => data_get($nodeEnv, 'value'), + ], + ]); + } + + $fields->put('Strapi', $data->toArray()); + break; + default: + $data = collect([]); + $admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first(); + // Chaskiq + $admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first(); + + $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first(); + if ($admin_user) { + $data = $data->merge([ + 'User' => [ + 'key' => data_get($admin_user, 'key'), + 'value' => data_get($admin_user, 'value', 'admin'), + 'readonly' => true, + 'rules' => 'required', + ], + ]); + } + if ($admin_password) { + $data = $data->merge([ + 'Password' => [ + 'key' => data_get($admin_password, 'key'), + 'value' => data_get($admin_password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + if ($admin_email) { + $data = $data->merge([ + 'Email' => [ + 'key' => data_get($admin_email, 'key'), + 'value' => data_get($admin_email, 'value'), + 'rules' => 'required|email', + ], + ]); + } + $fields->put('Admin', $data->toArray()); + break; } } $databases = $this->databases()->get(); foreach ($databases as $database) { - $image = str($database->image)->before(':')->value(); + $image = str($database->image)->before(':'); + if ($image->isEmpty()) { + continue; + } switch ($image) { - case str($image)->contains('postgres'): + case $image->contains('postgres'): $userVariables = ['SERVICE_USER_POSTGRES', 'SERVICE_USER_POSTGRESQL']; $passwordVariables = ['SERVICE_PASSWORD_POSTGRES', 'SERVICE_PASSWORD_POSTGRESQL']; $dbNameVariables = ['POSTGRESQL_DATABASE', 'POSTGRES_DB']; @@ -595,10 +999,10 @@ class Service extends BaseModel } $fields->put('PostgreSQL', $data->toArray()); break; - case str($image)->contains('mysql'): - $userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS']; - $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS']; - $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT']; + case $image->contains('mysql'): + $userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS', 'MYSQL_USER']; + $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD', 'SERVICE_PASSWORD_64_MYSQL']; + $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT', 'SERVICE_PASSWORD_64_MYSQLROOT']; $dbNameVariables = ['MYSQL_DATABASE']; $mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first(); $mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first(); @@ -645,11 +1049,11 @@ class Service extends BaseModel } $fields->put('MySQL', $data->toArray()); break; - case str($image)->contains('mariadb'): - $userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER']; - $passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS']; - $rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS']; - $dbNameVariables = ['SERVICE_DATABASE_MARIADB', 'SERVICE_DATABASE_WORDPRESS', '_APP_DB_SCHEMA']; + case $image->contains('mariadb'): + $userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER', 'SERVICE_USER_MYSQL', 'MYSQL_USER']; + $passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS', 'MYSQL_PASSWORD']; + $rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS', 'MYSQL_ROOT_PASSWORD']; + $dbNameVariables = ['SERVICE_DATABASE_MARIADB', 'SERVICE_DATABASE_WORDPRESS', '_APP_DB_SCHEMA', 'MYSQL_DATABASE']; $mariadb_user = $this->environment_variables()->whereIn('key', $userVariables)->first(); $mariadb_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first(); $mariadb_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first(); @@ -739,12 +1143,24 @@ class Service extends BaseModel public function failedTaskLink($task_uuid) { if (data_get($this, 'environment.project.uuid')) { - return route('project.service.scheduled-tasks', [ + $route = route('project.service.scheduled-tasks', [ 'project_uuid' => data_get($this, 'environment.project.uuid'), 'environment_name' => data_get($this, 'environment.name'), 'service_uuid' => data_get($this, 'uuid'), 'task_uuid' => $task_uuid, ]); + $settings = InstanceSettings::get(); + if (data_get($settings, 'fqdn')) { + $url = Url::fromString($route); + $url = $url->withPort(null); + $fqdn = data_get($settings, 'fqdn'); + $fqdn = str_replace(['http://', 'https://'], '', $fqdn); + $url = $url->withHost($fqdn); + + return $url->__toString(); + } + + return $route; } return null; @@ -818,12 +1234,12 @@ class Service extends BaseModel public function environment_variables(): HasMany { - return $this->hasMany(EnvironmentVariable::class)->orderBy('key', 'asc'); + return $this->hasMany(EnvironmentVariable::class)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC"); } public function environment_variables_preview(): HasMany { - return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderBy('key', 'asc'); + return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC"); } public function workdir() @@ -834,17 +1250,50 @@ class Service extends BaseModel public function saveComposeConfigs() { $workdir = $this->workdir(); - $commands[] = "mkdir -p $workdir"; - $commands[] = "cd $workdir"; - $docker_compose_base64 = base64_encode($this->docker_compose); - $commands[] = "echo $docker_compose_base64 | base64 -d | tee docker-compose.yml > /dev/null"; - $envs = $this->environment_variables()->get(); + instant_remote_process([ + "mkdir -p $workdir", + "cd $workdir", + ], $this->server); + + $filename = new Cuid2.'-docker-compose.yml'; + Storage::disk('local')->put("tmp/{$filename}", $this->docker_compose); + $path = Storage::path("tmp/{$filename}"); + instant_scp($path, "{$workdir}/docker-compose.yml", $this->server); + Storage::disk('local')->delete("tmp/{$filename}"); + + $commands[] = "cd $workdir"; $commands[] = 'rm -f .env || true'; - foreach ($envs as $env) { - $commands[] = "echo '{$env->key}={$env->real_value}' >> .env"; + + $envs_from_coolify = $this->environment_variables()->get(); + $sorted = $envs_from_coolify->sortBy(function ($env) { + if (str($env->key)->startsWith('SERVICE_')) { + return 1; + } + if (str($env->value)->startsWith('$SERVICE_') || str($env->value)->startsWith('${SERVICE_')) { + return 2; + } + + return 3; + }); + foreach ($sorted as $env) { + if (version_compare($env->version, '4.0.0-beta.347', '<=')) { + $commands[] = "echo '{$env->key}={$env->real_value}' >> .env"; + } else { + $real_value = $env->real_value; + if ($env->version === '4.0.0-beta.239') { + $real_value = $env->real_value; + } else { + if ($env->is_literal || $env->is_multiline) { + $real_value = '\''.$real_value.'\''; + } else { + $real_value = escapeEnvVariables($env->real_value); + } + } + $commands[] = "echo \"{$env->key}={$real_value}\" >> .env"; + } } - if ($envs->count() === 0) { + if ($sorted->count() === 0) { $commands[] = 'touch .env'; } instant_remote_process($commands, $this->server); @@ -852,14 +1301,33 @@ class Service extends BaseModel public function parse(bool $isNew = false): Collection { - return parseDockerComposeFile($this, $isNew); + if ((int) $this->compose_parsing_version >= 3) { + return newParser($this); + } elseif ($this->docker_compose_raw) { + return parseDockerComposeFile($this, $isNew); + } else { + return collect([]); + } } public function networks() { - $networks = getTopLevelNetworks($this); + return getTopLevelNetworks($this); + } - // ray($networks); - return $networks; + protected function isDeployable(): Attribute + { + return Attribute::make( + get: function () { + $envs = $this->environment_variables()->where('is_required', true)->get(); + foreach ($envs as $env) { + if ($env->is_really_required) { + return false; + } + } + + return true; + } + ); } } diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php index 98c1cf4e7..5cafc9042 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -19,6 +19,11 @@ class ServiceApplication extends BaseModel $service->persistentStorages()->delete(); $service->fileStorages()->delete(); }); + static::saving(function ($service) { + if ($service->isDirty('status')) { + $service->forceFill(['last_online_at' => now()]); + } + }); } public function restart() @@ -27,6 +32,26 @@ class ServiceApplication extends BaseModel instant_remote_process(["docker restart {$container_id}"], $this->service->server); } + public static function ownedByCurrentTeamAPI(int $teamId) + { + return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name'); + } + + public static function ownedByCurrentTeam() + { + return ServiceApplication::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + + public function isRunning() + { + return str($this->status)->contains('running'); + } + + public function isExited() + { + return str($this->status)->contains('exited'); + } + public function isLogDrainEnabled() { return data_get($this, 'is_log_drain_enabled', false); @@ -97,4 +122,9 @@ class ServiceApplication extends BaseModel { getFilesystemVolumesFromServer($this, $isInit); } + + public function isBackupSolutionAvailable() + { + return false; + } } diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php index 4a749913e..5fdd52637 100644 --- a/app/Models/ServiceDatabase.php +++ b/app/Models/ServiceDatabase.php @@ -17,6 +17,21 @@ class ServiceDatabase extends BaseModel $service->persistentStorages()->delete(); $service->fileStorages()->delete(); }); + static::saving(function ($service) { + if ($service->isDirty('status')) { + $service->forceFill(['last_online_at' => now()]); + } + }); + } + + public static function ownedByCurrentTeamAPI(int $teamId) + { + return ServiceDatabase::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name'); + } + + public static function ownedByCurrentTeam() + { + return ServiceDatabase::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name'); } public function restart() @@ -25,6 +40,16 @@ class ServiceDatabase extends BaseModel remote_process(["docker restart {$container_id}"], $this->service->server); } + public function isRunning() + { + return str($this->status)->contains('running'); + } + + public function isExited() + { + return str($this->status)->contains('exited'); + } + public function isLogDrainEnabled() { return data_get($this, 'is_log_drain_enabled', false); @@ -105,4 +130,13 @@ class ServiceDatabase extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function isBackupSolutionAvailable() + { + return str($this->databaseType())->contains('mysql') || + str($this->databaseType())->contains('postgres') || + str($this->databaseType())->contains('postgis') || + str($this->databaseType())->contains('mariadb') || + str($this->databaseType())->contains('mongodb'); + } } diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index c5e252c34..6d66c6854 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; @@ -13,6 +14,8 @@ class StandaloneClickhouse extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; + protected $casts = [ 'clickhouse_password' => 'encrypted', ]; @@ -29,19 +32,26 @@ class StandaloneClickhouse extends BaseModel 'is_readonly' => true, ]); }); - static::deleting(function ($database) { - $storages = $database->persistentStorages()->get(); - $server = data_get($database, 'destination.server'); - if ($server) { - foreach ($storages as $storage) { - instant_remote_process(["docker volume rm -f $storage->name"], $server, false); - } - } - $database->scheduledBackups()->delete(); + static::forceDeleting(function ($database) { $database->persistentStorages()->delete(); + $database->scheduledBackups()->delete(); $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); + } + + protected function serverStatus(): Attribute + { + return Attribute::make( + get: function () { + return $this->destination->server->isFunctional(); + } + ); } public function isConfigurationChanged(bool $save = false) @@ -70,6 +80,11 @@ class StandaloneClickhouse extends BaseModel } } + public function isRunning() + { + return (bool) str($this->status)->contains('running'); + } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); @@ -89,6 +104,17 @@ class StandaloneClickhouse extends BaseModel } } + public function delete_volumes(Collection $persistentStorages) + { + if ($persistentStorages->count() === 0) { + return; + } + $server = data_get($this, 'destination.server'); + foreach ($persistentStorages as $storage) { + instant_remote_process(["docker volume rm -f $storage->name"], $server, false); + } + } + public function realStatus() { return $this->getRawOriginal('status'); @@ -178,18 +204,36 @@ class StandaloneClickhouse extends BaseModel return data_get($this, 'environment.project.team'); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-clickhouse'; } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute { - if ($this->is_public && ! $useInternal) { - return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}"; - } else { - return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->uuid}:9000/{$this->clickhouse_db}"; - } + return new Attribute( + get: fn () => "clickhouse://{$this->clickhouse_admin_user}:{$this->clickhouse_admin_password}@{$this->uuid}:9000/{$this->clickhouse_db}", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "clickhouse://{$this->clickhouse_admin_user}:{$this->clickhouse_admin_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}"; + } + + return null; + } + ); } public function environment() @@ -226,4 +270,53 @@ class StandaloneClickhouse extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getCpuMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); + + return $parsedCollection->toArray(); + } + + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); + } + + public function isBackupSolutionAvailable() + { + return false; + } } diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index 8c739d984..f7d83f0a3 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; @@ -13,6 +14,8 @@ class StandaloneDragonfly extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; + protected $casts = [ 'dragonfly_password' => 'encrypted', ]; @@ -29,19 +32,26 @@ class StandaloneDragonfly extends BaseModel 'is_readonly' => true, ]); }); - static::deleting(function ($database) { - $database->scheduledBackups()->delete(); - $storages = $database->persistentStorages()->get(); - $server = data_get($database, 'destination.server'); - if ($server) { - foreach ($storages as $storage) { - instant_remote_process(["docker volume rm -f $storage->name"], $server, false); - } - } + static::forceDeleting(function ($database) { $database->persistentStorages()->delete(); + $database->scheduledBackups()->delete(); $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); + } + + protected function serverStatus(): Attribute + { + return Attribute::make( + get: function () { + return $this->destination->server->isFunctional(); + } + ); } public function isConfigurationChanged(bool $save = false) @@ -70,6 +80,11 @@ class StandaloneDragonfly extends BaseModel } } + public function isRunning() + { + return (bool) str($this->status)->contains('running'); + } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); @@ -89,6 +104,17 @@ class StandaloneDragonfly extends BaseModel } } + public function delete_volumes(Collection $persistentStorages) + { + if ($persistentStorages->count() === 0) { + return; + } + $server = data_get($this, 'destination.server'); + foreach ($persistentStorages as $storage) { + instant_remote_process(["docker volume rm -f $storage->name"], $server, false); + } + } + public function realStatus() { return $this->getRawOriginal('status'); @@ -178,18 +204,36 @@ class StandaloneDragonfly extends BaseModel ); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-dragonfly'; } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute { - if ($this->is_public && ! $useInternal) { - return "redis://:{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; - } else { - return "redis://:{$this->dragonfly_password}@{$this->uuid}:6379/0"; - } + return new Attribute( + get: fn () => "redis://:{$this->dragonfly_password}@{$this->uuid}:6379/0", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "redis://:{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + } + + return null; + } + ); } public function environment() @@ -226,4 +270,53 @@ class StandaloneDragonfly extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getCpuMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); + + return $parsedCollection->toArray(); + } + + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); + } + + public function isBackupSolutionAvailable() + { + return false; + } } diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 5216681c9..083c743d9 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; @@ -13,6 +14,8 @@ class StandaloneKeydb extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'server_status']; + protected $casts = [ 'keydb_password' => 'encrypted', ]; @@ -29,19 +32,26 @@ class StandaloneKeydb extends BaseModel 'is_readonly' => true, ]); }); - static::deleting(function ($database) { - $database->scheduledBackups()->delete(); - $storages = $database->persistentStorages()->get(); - $server = data_get($database, 'destination.server'); - if ($server) { - foreach ($storages as $storage) { - instant_remote_process(["docker volume rm -f $storage->name"], $server, false); - } - } + static::forceDeleting(function ($database) { $database->persistentStorages()->delete(); + $database->scheduledBackups()->delete(); $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); + } + + protected function serverStatus(): Attribute + { + return Attribute::make( + get: function () { + return $this->destination->server->isFunctional(); + } + ); } public function isConfigurationChanged(bool $save = false) @@ -70,6 +80,11 @@ class StandaloneKeydb extends BaseModel } } + public function isRunning() + { + return (bool) str($this->status)->contains('running'); + } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); @@ -89,6 +104,17 @@ class StandaloneKeydb extends BaseModel } } + public function delete_volumes(Collection $persistentStorages) + { + if ($persistentStorages->count() === 0) { + return; + } + $server = data_get($this, 'destination.server'); + foreach ($persistentStorages as $storage) { + instant_remote_process(["docker volume rm -f $storage->name"], $server, false); + } + } + public function realStatus() { return $this->getRawOriginal('status'); @@ -178,18 +204,36 @@ class StandaloneKeydb extends BaseModel ); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-keydb'; } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute { - if ($this->is_public && ! $useInternal) { - return "redis://{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; - } else { - return "redis://{$this->keydb_password}@{$this->uuid}:6379/0"; - } + return new Attribute( + get: fn () => "redis://:{$this->keydb_password}@{$this->uuid}:6379/0", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "redis://:{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + } + + return null; + } + ); } public function environment() @@ -226,4 +270,53 @@ class StandaloneKeydb extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getCpuMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); + + return $parsedCollection->toArray(); + } + + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); + } + + public function isBackupSolutionAvailable() + { + return false; + } } diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index 33fd2cbc2..833dad6c4 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; @@ -13,6 +14,8 @@ class StandaloneMariadb extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; + protected $casts = [ 'mariadb_password' => 'encrypted', ]; @@ -29,19 +32,26 @@ class StandaloneMariadb extends BaseModel 'is_readonly' => true, ]); }); - static::deleting(function ($database) { - $storages = $database->persistentStorages()->get(); - $server = data_get($database, 'destination.server'); - if ($server) { - foreach ($storages as $storage) { - instant_remote_process(["docker volume rm -f $storage->name"], $server, false); - } - } - $database->scheduledBackups()->delete(); + static::forceDeleting(function ($database) { $database->persistentStorages()->delete(); + $database->scheduledBackups()->delete(); $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); + } + + protected function serverStatus(): Attribute + { + return Attribute::make( + get: function () { + return $this->destination->server->isFunctional(); + } + ); } public function isConfigurationChanged(bool $save = false) @@ -70,6 +80,11 @@ class StandaloneMariadb extends BaseModel } } + public function isRunning() + { + return (bool) str($this->status)->contains('running'); + } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); @@ -89,6 +104,17 @@ class StandaloneMariadb extends BaseModel } } + public function delete_volumes(Collection $persistentStorages) + { + if ($persistentStorages->count() === 0) { + return; + } + $server = data_get($this, 'destination.server'); + foreach ($persistentStorages as $storage) { + instant_remote_process(["docker volume rm -f $storage->name"], $server, false); + } + } + public function realStatus() { return $this->getRawOriginal('status'); @@ -161,6 +187,13 @@ class StandaloneMariadb extends BaseModel return data_get($this, 'is_log_drain_enabled', false); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-mariadb'; @@ -183,13 +216,24 @@ class StandaloneMariadb extends BaseModel ); } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute { - if ($this->is_public && ! $useInternal) { - return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}"; - } else { - return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}"; - } + return new Attribute( + get: fn () => "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}"; + } + + return null; + } + ); } public function environment() @@ -226,4 +270,53 @@ class StandaloneMariadb extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getCpuMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); + + return $parsedCollection->toArray(); + } + + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); + } + + public function isBackupSolutionAvailable() + { + return true; + } } diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 0cc52b3f7..dd8893180 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; @@ -13,6 +14,8 @@ class StandaloneMongodb extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; + protected static function booted() { static::created(function ($database) { @@ -33,19 +36,26 @@ class StandaloneMongodb extends BaseModel 'is_readonly' => true, ]); }); - static::deleting(function ($database) { - $storages = $database->persistentStorages()->get(); - $server = data_get($database, 'destination.server'); - if ($server) { - foreach ($storages as $storage) { - instant_remote_process(["docker volume rm -f $storage->name"], $server, false); - } - } - $database->scheduledBackups()->delete(); + static::forceDeleting(function ($database) { $database->persistentStorages()->delete(); + $database->scheduledBackups()->delete(); $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); + } + + protected function serverStatus(): Attribute + { + return Attribute::make( + get: function () { + return $this->destination->server->isFunctional(); + } + ); } public function isConfigurationChanged(bool $save = false) @@ -74,6 +84,11 @@ class StandaloneMongodb extends BaseModel } } + public function isRunning() + { + return (bool) str($this->status)->contains('running'); + } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); @@ -93,6 +108,17 @@ class StandaloneMongodb extends BaseModel } } + public function delete_volumes(Collection $persistentStorages) + { + if ($persistentStorages->count() === 0) { + return; + } + $server = data_get($this, 'destination.server'); + foreach ($persistentStorages as $storage) { + instant_remote_process(["docker volume rm -f $storage->name"], $server, false); + } + } + public function realStatus() { return $this->getRawOriginal('status'); @@ -198,18 +224,36 @@ class StandaloneMongodb extends BaseModel ); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-mongodb'; } - public function get_db_url(bool $useInternal = false) + protected function internalDbUrl(): Attribute { - if ($this->is_public && ! $useInternal) { - return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true"; - } else { - return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true"; - } + return new Attribute( + get: fn () => "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true"; + } + + return null; + } + ); } public function environment() @@ -246,4 +290,53 @@ class StandaloneMongodb extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getCpuMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); + + return $parsedCollection->toArray(); + } + + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); + } + + public function isBackupSolutionAvailable() + { + return true; + } } diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index 174736f77..710fea1bc 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; @@ -13,6 +14,8 @@ class StandaloneMysql extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; + protected $casts = [ 'mysql_password' => 'encrypted', 'mysql_root_password' => 'encrypted', @@ -30,19 +33,26 @@ class StandaloneMysql extends BaseModel 'is_readonly' => true, ]); }); - static::deleting(function ($database) { - $storages = $database->persistentStorages()->get(); - $server = data_get($database, 'destination.server'); - if ($server) { - foreach ($storages as $storage) { - instant_remote_process(["docker volume rm -f $storage->name"], $server, false); - } - } - $database->scheduledBackups()->delete(); + static::forceDeleting(function ($database) { $database->persistentStorages()->delete(); + $database->scheduledBackups()->delete(); $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); + } + + protected function serverStatus(): Attribute + { + return Attribute::make( + get: function () { + return $this->destination->server->isFunctional(); + } + ); } public function isConfigurationChanged(bool $save = false) @@ -71,6 +81,11 @@ class StandaloneMysql extends BaseModel } } + public function isRunning() + { + return (bool) str($this->status)->contains('running'); + } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); @@ -90,6 +105,17 @@ class StandaloneMysql extends BaseModel } } + public function delete_volumes(Collection $persistentStorages) + { + if ($persistentStorages->count() === 0) { + return; + } + $server = data_get($this, 'destination.server'); + foreach ($persistentStorages as $storage) { + instant_remote_process(["docker volume rm -f $storage->name"], $server, false); + } + } + public function realStatus() { return $this->getRawOriginal('status'); @@ -157,6 +183,13 @@ class StandaloneMysql extends BaseModel return null; } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-mysql'; @@ -184,13 +217,24 @@ class StandaloneMysql extends BaseModel ); } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute { - if ($this->is_public && ! $useInternal) { - return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}"; - } else { - return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->uuid}:3306/{$this->mysql_database}"; - } + return new Attribute( + get: fn () => "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->uuid}:3306/{$this->mysql_database}", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}"; + } + + return null; + } + ); } public function environment() @@ -227,4 +271,53 @@ class StandaloneMysql extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getCpuMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); + + return $parsedCollection->toArray(); + } + + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); + } + + public function isBackupSolutionAvailable() + { + return true; + } } diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index a5bf4dc2a..4a457a6cf 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; @@ -13,6 +14,8 @@ class StandalonePostgresql extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; + protected $casts = [ 'init_scripts' => 'array', 'postgres_password' => 'encrypted', @@ -30,19 +33,17 @@ class StandalonePostgresql extends BaseModel 'is_readonly' => true, ]); }); - static::deleting(function ($database) { - $storages = $database->persistentStorages()->get(); - $server = data_get($database, 'destination.server'); - if ($server) { - foreach ($storages as $storage) { - instant_remote_process(["docker volume rm -f $storage->name"], $server, false); - } - } - $database->scheduledBackups()->delete(); + static::forceDeleting(function ($database) { $database->persistentStorages()->delete(); + $database->scheduledBackups()->delete(); $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); } public function workdir() @@ -50,6 +51,15 @@ class StandalonePostgresql extends BaseModel return database_configuration_dir()."/{$this->uuid}"; } + protected function serverStatus(): Attribute + { + return Attribute::make( + get: function () { + return $this->destination->server->isFunctional(); + } + ); + } + public function delete_configurations() { $server = data_get($this, 'destination.server'); @@ -59,6 +69,17 @@ class StandalonePostgresql extends BaseModel } } + public function delete_volumes(Collection $persistentStorages) + { + if ($persistentStorages->count() === 0) { + return; + } + $server = data_get($this, 'destination.server'); + foreach ($persistentStorages as $storage) { + instant_remote_process(["docker volume rm -f $storage->name"], $server, false); + } + } + public function isConfigurationChanged(bool $save = false) { $newConfigHash = $this->image.$this->ports_mappings.$this->postgres_initdb_args.$this->postgres_host_auth_method; @@ -85,6 +106,11 @@ class StandalonePostgresql extends BaseModel } } + public function isRunning() + { + return (bool) str($this->status)->contains('running'); + } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); @@ -179,18 +205,36 @@ class StandalonePostgresql extends BaseModel return data_get($this, 'environment.project.team'); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-postgresql'; } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute { - if ($this->is_public && ! $useInternal) { - return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}"; - } else { - return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}"; - } + return new Attribute( + get: fn () => "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}"; + } + + return null; + } + ); } public function environment() @@ -227,4 +271,53 @@ class StandalonePostgresql extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function isBackupSolutionAvailable() + { + return true; + } + + public function getCpuMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); + + return $parsedCollection->toArray(); + } + + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); + } } diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index ed379750e..826bb951c 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; @@ -13,6 +14,8 @@ class StandaloneRedis extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; + protected static function booted() { static::created(function ($database) { @@ -25,19 +28,26 @@ class StandaloneRedis extends BaseModel 'is_readonly' => true, ]); }); - static::deleting(function ($database) { - $database->scheduledBackups()->delete(); - $storages = $database->persistentStorages()->get(); - $server = data_get($database, 'destination.server'); - if ($server) { - foreach ($storages as $storage) { - instant_remote_process(["docker volume rm -f $storage->name"], $server, false); - } - } + static::forceDeleting(function ($database) { $database->persistentStorages()->delete(); + $database->scheduledBackups()->delete(); $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); + } + + protected function serverStatus(): Attribute + { + return Attribute::make( + get: function () { + return $this->destination->server->isFunctional(); + } + ); } public function isConfigurationChanged(bool $save = false) @@ -66,6 +76,11 @@ class StandaloneRedis extends BaseModel } } + public function isRunning() + { + return (bool) str($this->status)->contains('running'); + } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); @@ -85,6 +100,17 @@ class StandaloneRedis extends BaseModel } } + public function delete_volumes(Collection $persistentStorages) + { + if ($persistentStorages->count() === 0) { + return; + } + $server = data_get($this, 'destination.server'); + foreach ($persistentStorages as $storage) { + instant_remote_process(["docker volume rm -f $storage->name"], $server, false); + } + } + public function realStatus() { return $this->getRawOriginal('status'); @@ -179,13 +205,46 @@ class StandaloneRedis extends BaseModel return 'standalone-redis'; } - public function get_db_url(bool $useInternal = false): string + public function databaseType(): Attribute { - if ($this->is_public && ! $useInternal) { - return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; - } else { - return "redis://:{$this->redis_password}@{$this->uuid}:6379/0"; - } + return new Attribute( + get: fn () => $this->type(), + ); + } + + protected function internalDbUrl(): Attribute + { + return new Attribute( + get: function () { + $redis_version = $this->getRedisVersion(); + $username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : ''; + + return "redis://{$username_part}{$this->redis_password}@{$this->uuid}:6379/0"; + } + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + $redis_version = $this->getRedisVersion(); + $username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : ''; + + return "redis://{$username_part}{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + } + + return null; + } + ); + } + + public function getRedisVersion() + { + $image_parts = explode(':', $this->image); + + return $image_parts[1] ?? '0.0'; } public function environment() @@ -222,4 +281,82 @@ class StandaloneRedis extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getCpuMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); + + return $parsedCollection->toArray(); + } + + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); + } + + public function isBackupSolutionAvailable() + { + return false; + } + + public function redisPassword(): Attribute + { + return new Attribute( + get: function () { + $password = $this->runtime_environment_variables()->where('key', 'REDIS_PASSWORD')->first(); + if (! $password) { + return null; + } + + return $password->value; + }, + + ); + } + + public function redisUsername(): Attribute + { + return new Attribute( + get: function () { + $username = $this->runtime_environment_variables()->where('key', 'REDIS_USERNAME')->first(); + if (! $username) { + return null; + } + + return $username->value; + } + ); + } } diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php index 35dc43c0c..1bd84a664 100644 --- a/app/Models/Subscription.php +++ b/app/Models/Subscription.php @@ -15,22 +15,7 @@ class Subscription extends Model public function type() { - if (isLemon()) { - $basic = explode(',', config('subscription.lemon_squeezy_basic_plan_ids')); - $pro = explode(',', config('subscription.lemon_squeezy_pro_plan_ids')); - $ultimate = explode(',', config('subscription.lemon_squeezy_ultimate_plan_ids')); - - $subscription = $this->lemon_variant_id; - if (in_array($subscription, $basic)) { - return 'basic'; - } - if (in_array($subscription, $pro)) { - return 'pro'; - } - if (in_array($subscription, $ultimate)) { - return 'ultimate'; - } - } elseif (isStripe()) { + if (isStripe()) { if (! $this->stripe_plan_id) { return 'zero'; } diff --git a/app/Models/Team.php b/app/Models/Team.php index fe5995a1b..8996b745c 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -7,7 +7,69 @@ use App\Notifications\Channels\SendsEmail; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; +use OpenApi\Attributes as OA; +#[OA\Schema( + description: 'Team model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer', 'description' => 'The unique identifier of the team.'], + 'name' => ['type' => 'string', 'description' => 'The name of the team.'], + 'description' => ['type' => 'string', 'description' => 'The description of the team.'], + 'personal_team' => ['type' => 'boolean', 'description' => 'Whether the team is personal or not.'], + 'created_at' => ['type' => 'string', 'description' => 'The date and time the team was created.'], + 'updated_at' => ['type' => 'string', 'description' => 'The date and time the team was last updated.'], + 'smtp_enabled' => ['type' => 'boolean', 'description' => 'Whether SMTP is enabled or not.'], + 'smtp_from_address' => ['type' => 'string', 'description' => 'The email address to send emails from.'], + 'smtp_from_name' => ['type' => 'string', 'description' => 'The name to send emails from.'], + 'smtp_recipients' => ['type' => 'string', 'description' => 'The email addresses to send emails to.'], + 'smtp_host' => ['type' => 'string', 'description' => 'The SMTP host.'], + 'smtp_port' => ['type' => 'string', 'description' => 'The SMTP port.'], + 'smtp_encryption' => ['type' => 'string', 'description' => 'The SMTP encryption.'], + 'smtp_username' => ['type' => 'string', 'description' => 'The SMTP username.'], + 'smtp_password' => ['type' => 'string', 'description' => 'The SMTP password.'], + 'smtp_timeout' => ['type' => 'string', 'description' => 'The SMTP timeout.'], + 'smtp_notifications_test' => ['type' => 'boolean', 'description' => 'Whether to send test notifications via SMTP.'], + 'smtp_notifications_deployments' => ['type' => 'boolean', 'description' => 'Whether to send deployment notifications via SMTP.'], + 'smtp_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via SMTP.'], + 'smtp_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via SMTP.'], + 'smtp_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via SMTP.'], + 'smtp_notifications_server_disk_usage' => ['type' => 'boolean', 'description' => 'Whether to send server disk usage notifications via SMTP.'], + 'discord_enabled' => ['type' => 'boolean', 'description' => 'Whether Discord is enabled or not.'], + 'discord_webhook_url' => ['type' => 'string', 'description' => 'The Discord webhook URL.'], + 'discord_notifications_test' => ['type' => 'boolean', 'description' => 'Whether to send test notifications via Discord.'], + 'discord_notifications_deployments' => ['type' => 'boolean', 'description' => 'Whether to send deployment notifications via Discord.'], + 'discord_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via Discord.'], + 'discord_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via Discord.'], + 'discord_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via Discord.'], + 'discord_notifications_server_disk_usage' => ['type' => 'boolean', 'description' => 'Whether to send server disk usage notifications via Discord.'], + 'show_boarding' => ['type' => 'boolean', 'description' => 'Whether to show the boarding screen or not.'], + 'resend_enabled' => ['type' => 'boolean', 'description' => 'Whether to enable resending or not.'], + 'resend_api_key' => ['type' => 'string', 'description' => 'The resending API key.'], + 'use_instance_email_settings' => ['type' => 'boolean', 'description' => 'Whether to use instance email settings or not.'], + 'telegram_enabled' => ['type' => 'boolean', 'description' => 'Whether Telegram is enabled or not.'], + 'telegram_token' => ['type' => 'string', 'description' => 'The Telegram token.'], + 'telegram_chat_id' => ['type' => 'string', 'description' => 'The Telegram chat ID.'], + 'telegram_notifications_test' => ['type' => 'boolean', 'description' => 'Whether to send test notifications via Telegram.'], + 'telegram_notifications_deployments' => ['type' => 'boolean', 'description' => 'Whether to send deployment notifications via Telegram.'], + 'telegram_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via Telegram.'], + 'telegram_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via Telegram.'], + 'telegram_notifications_test_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram test message thread ID.'], + 'telegram_notifications_deployments_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram deployment message thread ID.'], + 'telegram_notifications_status_changes_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram status change message thread ID.'], + 'telegram_notifications_database_backups_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram database backup message thread ID.'], + + 'custom_server_limit' => ['type' => 'string', 'description' => 'The custom server limit.'], + 'telegram_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via Telegram.'], + 'telegram_notifications_scheduled_tasks_thread_id' => ['type' => 'string', 'description' => 'The Telegram scheduled task message thread ID.'], + 'members' => new OA\Property( + property: 'members', + type: 'array', + items: new OA\Items(ref: '#/components/schemas/User'), + description: 'The members of the team.' + ), + ] +)] class Team extends Model implements SendsDiscord, SendsEmail { use Notifiable; @@ -31,27 +93,22 @@ class Team extends Model implements SendsDiscord, SendsEmail static::deleting(function ($team) { $keys = $team->privateKeys; foreach ($keys as $key) { - ray('Deleting key: '.$key->name); $key->delete(); } $sources = $team->sources(); foreach ($sources as $source) { - ray('Deleting source: '.$source->name); $source->delete(); } $tags = Tag::whereTeamId($team->id)->get(); foreach ($tags as $tag) { - ray('Deleting tag: '.$tag->name); $tag->delete(); } $shared_variables = $team->environment_variables(); foreach ($shared_variables as $shared_variable) { - ray('Deleting team shared variable: '.$shared_variable->name); $shared_variable->delete(); } $s3s = $team->s3s; foreach ($s3s as $s3) { - ray('Deleting s3: '.$s3->name); $s3->delete(); } }); @@ -74,9 +131,7 @@ class Team extends Model implements SendsDiscord, SendsEmail { $recipients = data_get($notification, 'emails', null); if (is_null($recipients)) { - $recipients = $this->members()->pluck('email')->toArray(); - - return $recipients; + return $this->members()->pluck('email')->toArray(); } return explode(',', $recipients); @@ -105,8 +160,12 @@ class Team extends Model implements SendsDiscord, SendsEmail if (currentTeam()->id === 0 && isDev()) { return 9999999; } + $team = Team::find(currentTeam()->id); + if (! $team) { + return 0; + } - return Team::find(currentTeam()->id)->limits['serverLimit']; + return data_get($team, 'limits', 0); } public function limits(): Attribute @@ -128,9 +187,8 @@ class Team extends Model implements SendsDiscord, SendsEmail } else { $serverLimit = config('constants.limits.server')[strtolower($subscription)]; } - $sharedEmailEnabled = config('constants.limits.email')[strtolower($subscription)]; - return ['serverLimit' => $serverLimit, 'sharedEmailEnabled' => $sharedEmailEnabled]; + return $serverLimit ?? 2; } ); @@ -190,9 +248,8 @@ class Team extends Model implements SendsDiscord, SendsEmail $sources = collect([]); $github_apps = $this->hasMany(GithubApp::class)->whereisPublic(false)->get(); $gitlab_apps = $this->hasMany(GitlabApp::class)->whereisPublic(false)->get(); - $sources = $sources->merge($github_apps)->merge($gitlab_apps); - return $sources; + return $sources->merge($github_apps)->merge($gitlab_apps); } public function s3s() @@ -200,8 +257,15 @@ class Team extends Model implements SendsDiscord, SendsEmail return $this->hasMany(S3Storage::class)->where('is_usable', true); } - public function trialEnded() + public function subscriptionEnded() { + $this->subscription->update([ + 'stripe_subscription_id' => null, + 'stripe_plan_id' => null, + 'stripe_cancel_at_period_end' => false, + 'stripe_invoice_paid' => false, + 'stripe_trial_already_ended' => false, + ]); foreach ($this->servers as $server) { $server->settings()->update([ 'is_usable' => false, @@ -210,16 +274,6 @@ class Team extends Model implements SendsDiscord, SendsEmail } } - public function trialEndedButSubscribed() - { - foreach ($this->servers as $server) { - $server->settings()->update([ - 'is_usable' => true, - 'is_reachable' => true, - ]); - } - } - public function isAnyNotificationEnabled() { if (isCloud()) { diff --git a/app/Models/TeamInvitation.php b/app/Models/TeamInvitation.php index c202710e2..bc1a90d58 100644 --- a/app/Models/TeamInvitation.php +++ b/app/Models/TeamInvitation.php @@ -20,11 +20,16 @@ class TeamInvitation extends Model return $this->belongsTo(Team::class); } + public static function ownedByCurrentTeam() + { + return TeamInvitation::whereTeamId(currentTeam()->id); + } + public function isValid() { $createdAt = $this->created_at; - $diff = $createdAt->diffInMinutes(now()); - if ($diff <= config('constants.invitation.link.expiration')) { + $diff = $createdAt->diffInDays(now()); + if ($diff <= config('constants.invitation.link.expiration_days')) { return true; } else { $this->delete(); diff --git a/app/Models/User.php b/app/Models/User.php index 1e120e951..25fb33d66 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -10,6 +10,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\URL; @@ -17,7 +18,23 @@ use Illuminate\Support\Str; use Laravel\Fortify\TwoFactorAuthenticatable; use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\NewAccessToken; +use OpenApi\Attributes as OA; +#[OA\Schema( + description: 'User model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer', 'description' => 'The user identifier in the database.'], + 'name' => ['type' => 'string', 'description' => 'The user name.'], + 'email' => ['type' => 'string', 'description' => 'The user email.'], + 'email_verified_at' => ['type' => 'string', 'description' => 'The date when the user email was verified.'], + 'created_at' => ['type' => 'string', 'description' => 'The date when the user was created.'], + 'updated_at' => ['type' => 'string', 'description' => 'The date when the user was updated.'], + 'two_factor_confirmed_at' => ['type' => 'string', 'description' => 'The date when the user two factor was confirmed.'], + 'force_password_reset' => ['type' => 'boolean', 'description' => 'The flag to force the user to reset the password.'], + 'marketing_emails' => ['type' => 'boolean', 'description' => 'The flag to receive marketing emails.'], + ], +)] class User extends Authenticatable implements SendsEmail { use HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable; @@ -104,7 +121,7 @@ class User extends Authenticatable implements SendsEmail public function sendVerificationEmail() { - $mail = new MailMessage(); + $mail = new MailMessage; $url = Url::temporarySignedRoute( 'verify.verify', Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)), @@ -142,7 +159,7 @@ class User extends Authenticatable implements SendsEmail public function isAdminFromSession() { - if (auth()->user()->id === 0) { + if (Auth::id() === 0) { return true; } $teams = $this->teams()->get(); @@ -162,8 +179,12 @@ class User extends Authenticatable implements SendsEmail public function isInstanceAdmin() { - $found_root_team = auth()->user()->teams->filter(function ($team) { + $found_root_team = Auth::user()->teams->filter(function ($team) { if ($team->id == 0) { + if (! Auth::user()->isAdmin()) { + return false; + } + return true; } @@ -175,9 +196,9 @@ class User extends Authenticatable implements SendsEmail public function currentTeam() { - return Cache::remember('team:'.auth()->user()->id, 3600, function () { - if (is_null(data_get(session('currentTeam'), 'id')) && auth()->user()->teams->count() > 0) { - return auth()->user()->teams[0]; + return Cache::remember('team:'.Auth::id(), 3600, function () { + if (is_null(data_get(session('currentTeam'), 'id')) && Auth::user()->teams->count() > 0) { + return Auth::user()->teams[0]; } return Team::find(session('currentTeam')->id); @@ -186,7 +207,7 @@ class User extends Authenticatable implements SendsEmail public function otherTeams() { - return auth()->user()->teams->filter(function ($team) { + return Auth::user()->teams->filter(function ($team) { return $team->id != currentTeam()->id; }); } @@ -196,7 +217,7 @@ class User extends Authenticatable implements SendsEmail if (data_get($this, 'pivot')) { return $this->pivot->role; } - $user = auth()->user()->teams->where('id', currentTeam()->id)->first(); + $user = Auth::user()->teams->where('id', currentTeam()->id)->first(); return data_get($user, 'pivot.role'); } diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php index 1858f31e0..242980e00 100644 --- a/app/Notifications/Application/DeploymentFailed.php +++ b/app/Notifications/Application/DeploymentFailed.php @@ -4,11 +4,11 @@ namespace App\Notifications\Application; use App\Models\Application; use App\Models\ApplicationPreview; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; -use Illuminate\Support\Str; class DeploymentFailed extends Notification implements ShouldQueue { @@ -41,8 +41,8 @@ class DeploymentFailed extends Notification implements ShouldQueue $this->project_uuid = data_get($application, 'environment.project.uuid'); $this->environment_name = data_get($application, 'environment.name'); $this->fqdn = data_get($application, 'fqdn'); - if (Str::of($this->fqdn)->explode(',')->count() > 1) { - $this->fqdn = Str::of($this->fqdn)->explode(',')->first(); + if (str($this->fqdn)->explode(',')->count() > 1) { + $this->fqdn = str($this->fqdn)->explode(',')->first(); } $this->deployment_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}"; } @@ -54,7 +54,7 @@ class DeploymentFailed extends Notification implements ShouldQueue public function toMail(): MailMessage { - $mail = new MailMessage(); + $mail = new MailMessage; $pull_request_id = data_get($this->preview, 'pull_request_id', 0); $fqdn = $this->fqdn; if ($pull_request_id === 0) { @@ -73,14 +73,42 @@ class DeploymentFailed extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { if ($this->preview) { - $message = 'Coolify: Pull request #'.$this->preview->pull_request_id.' of '.$this->application_name.' ('.$this->preview->fqdn.') deployment failed: '; - $message .= '[View Deployment Logs]('.$this->deployment_url.')'; + $message = new DiscordMessage( + title: ':cross_mark: Deployment failed', + description: 'Pull request: '.$this->preview->pull_request_id, + color: DiscordMessage::errorColor(), + isCritical: true, + ); + + $message->addField('Project', data_get($this->application, 'environment.project.name'), true); + $message->addField('Environment', $this->environment_name, true); + $message->addField('Name', $this->application_name, true); + + $message->addField('Deployment Logs', '[Link]('.$this->deployment_url.')'); + if ($this->fqdn) { + $message->addField('Domain', $this->fqdn, true); + } } else { - $message = 'Coolify: Deployment failed of '.$this->application_name.' ('.$this->fqdn.'): '; - $message .= '[View Deployment Logs]('.$this->deployment_url.')'; + if ($this->fqdn) { + $description = '[Open application]('.$this->fqdn.')'; + } else { + $description = ''; + } + $message = new DiscordMessage( + title: ':cross_mark: Deployment failed', + description: $description, + color: DiscordMessage::errorColor(), + isCritical: true, + ); + + $message->addField('Project', data_get($this->application, 'environment.project.name'), true); + $message->addField('Environment', $this->environment_name, true); + $message->addField('Name', $this->application_name, true); + + $message->addField('Deployment Logs', '[Link]('.$this->deployment_url.')'); } return $message; diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php index 0cac6cbab..946a622ca 100644 --- a/app/Notifications/Application/DeploymentSuccess.php +++ b/app/Notifications/Application/DeploymentSuccess.php @@ -4,11 +4,11 @@ namespace App\Notifications\Application; use App\Models\Application; use App\Models\ApplicationPreview; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; -use Illuminate\Support\Str; class DeploymentSuccess extends Notification implements ShouldQueue { @@ -41,8 +41,8 @@ class DeploymentSuccess extends Notification implements ShouldQueue $this->project_uuid = data_get($application, 'environment.project.uuid'); $this->environment_name = data_get($application, 'environment.name'); $this->fqdn = data_get($application, 'fqdn'); - if (Str::of($this->fqdn)->explode(',')->count() > 1) { - $this->fqdn = Str::of($this->fqdn)->explode(',')->first(); + if (str($this->fqdn)->explode(',')->count() > 1) { + $this->fqdn = str($this->fqdn)->explode(',')->first(); } $this->deployment_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}"; } @@ -52,7 +52,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue $channels = setNotificationChannels($notifiable, 'deployments'); if (isCloud()) { // TODO: Make batch notifications work with email - $channels = array_diff($channels, ['App\Notifications\Channels\EmailChannel']); + $channels = array_diff($channels, [\App\Notifications\Channels\EmailChannel::class]); } return $channels; @@ -60,7 +60,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue public function toMail(): MailMessage { - $mail = new MailMessage(); + $mail = new MailMessage; $pull_request_id = data_get($this->preview, 'pull_request_id', 0); $fqdn = $this->fqdn; if ($pull_request_id === 0) { @@ -79,24 +79,39 @@ class DeploymentSuccess extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { if ($this->preview) { - $message = 'Coolify: New PR'.$this->preview->pull_request_id.' version successfully deployed of '.$this->application_name.' + $message = new DiscordMessage( + title: ':white_check_mark: Preview deployment successful', + description: 'Pull request: '.$this->preview->pull_request_id, + color: DiscordMessage::successColor(), + ); -'; if ($this->preview->fqdn) { - $message .= '[Open Application]('.$this->preview->fqdn.') | '; + $message->addField('Application', '[Link]('.$this->preview->fqdn.')'); } - $message .= '[Deployment logs]('.$this->deployment_url.')'; - } else { - $message = 'Coolify: New version successfully deployed of '.$this->application_name.' -'; + $message->addField('Project', data_get($this->application, 'environment.project.name'), true); + $message->addField('Environment', $this->environment_name, true); + $message->addField('Name', $this->application_name, true); + $message->addField('Deployment logs', '[Link]('.$this->deployment_url.')'); + } else { if ($this->fqdn) { - $message .= '[Open Application]('.$this->fqdn.') | '; + $description = '[Open application]('.$this->fqdn.')'; + } else { + $description = ''; } - $message .= '[Deployment logs]('.$this->deployment_url.')'; + $message = new DiscordMessage( + title: ':white_check_mark: New version successfully deployed', + description: $description, + color: DiscordMessage::successColor(), + ); + $message->addField('Project', data_get($this->application, 'environment.project.name'), true); + $message->addField('Environment', $this->environment_name, true); + $message->addField('Name', $this->application_name, true); + + $message->addField('Deployment logs', '[Link]('.$this->deployment_url.')'); } return $message; diff --git a/app/Notifications/Application/StatusChanged.php b/app/Notifications/Application/StatusChanged.php index baf508895..852c6b526 100644 --- a/app/Notifications/Application/StatusChanged.php +++ b/app/Notifications/Application/StatusChanged.php @@ -3,11 +3,11 @@ namespace App\Notifications\Application; use App\Models\Application; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; -use Illuminate\Support\Str; class StatusChanged extends Notification implements ShouldQueue { @@ -31,8 +31,8 @@ class StatusChanged extends Notification implements ShouldQueue $this->project_uuid = data_get($resource, 'environment.project.uuid'); $this->environment_name = data_get($resource, 'environment.name'); $this->fqdn = data_get($resource, 'fqdn', null); - if (Str::of($this->fqdn)->explode(',')->count() > 1) { - $this->fqdn = Str::of($this->fqdn)->explode(',')->first(); + if (str($this->fqdn)->explode(',')->count() > 1) { + $this->fqdn = str($this->fqdn)->explode(',')->first(); } $this->resource_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->resource->uuid}"; } @@ -44,7 +44,7 @@ class StatusChanged extends Notification implements ShouldQueue public function toMail(): MailMessage { - $mail = new MailMessage(); + $mail = new MailMessage; $fqdn = $this->fqdn; $mail->subject("Coolify: {$this->resource_name} has been stopped"); $mail->view('emails.application-status-changes', [ @@ -56,14 +56,14 @@ class StatusChanged extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = 'Coolify: '.$this->resource_name.' has been stopped. - -'; - $message .= '[Open Application in Coolify]('.$this->resource_url.')'; - - return $message; + return new DiscordMessage( + title: ':cross_mark: Application stopped', + description: '[Open Application in Coolify]('.$this->resource_url.')', + color: DiscordMessage::errorColor(), + isCritical: true, + ); } public function toTelegram(): array diff --git a/app/Notifications/Channels/DiscordChannel.php b/app/Notifications/Channels/DiscordChannel.php index f1706f138..86276fec9 100644 --- a/app/Notifications/Channels/DiscordChannel.php +++ b/app/Notifications/Channels/DiscordChannel.php @@ -12,11 +12,11 @@ class DiscordChannel */ public function send(SendsDiscord $notifiable, Notification $notification): void { - $message = $notification->toDiscord($notifiable); + $message = $notification->toDiscord(); $webhookUrl = $notifiable->routeNotificationForDiscord(); if (! $webhookUrl) { return; } - dispatch(new SendMessageToDiscordJob($message, $webhookUrl)); + dispatch(new SendMessageToDiscordJob($message, $webhookUrl))->onQueue('high'); } } diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index 413d3de53..af9af978d 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -32,7 +32,6 @@ class EmailChannel if ($error === 'No email settings found.') { throw $e; } - ray($e->getMessage()); $message = "EmailChannel error: {$e->getMessage()}. Failed to send email to:"; if (isset($recipients)) { $message .= implode(', ', $recipients); diff --git a/app/Notifications/Channels/TelegramChannel.php b/app/Notifications/Channels/TelegramChannel.php index b1a607651..b3d4e384b 100644 --- a/app/Notifications/Channels/TelegramChannel.php +++ b/app/Notifications/Channels/TelegramChannel.php @@ -18,29 +18,29 @@ class TelegramChannel $topicsInstance = get_class($notification); switch ($topicsInstance) { - case 'App\Notifications\Test': + case \App\Notifications\Test::class: $topicId = data_get($notifiable, 'telegram_notifications_test_message_thread_id'); break; - case 'App\Notifications\Application\StatusChanged': - case 'App\Notifications\Container\ContainerRestarted': - case 'App\Notifications\Container\ContainerStopped': + case \App\Notifications\Application\StatusChanged::class: + case \App\Notifications\Container\ContainerRestarted::class: + case \App\Notifications\Container\ContainerStopped::class: $topicId = data_get($notifiable, 'telegram_notifications_status_changes_message_thread_id'); break; - case 'App\Notifications\Application\DeploymentSuccess': - case 'App\Notifications\Application\DeploymentFailed': + case \App\Notifications\Application\DeploymentSuccess::class: + case \App\Notifications\Application\DeploymentFailed::class: $topicId = data_get($notifiable, 'telegram_notifications_deployments_message_thread_id'); break; - case 'App\Notifications\Database\BackupSuccess': - case 'App\Notifications\Database\BackupFailed': + case \App\Notifications\Database\BackupSuccess::class: + case \App\Notifications\Database\BackupFailed::class: $topicId = data_get($notifiable, 'telegram_notifications_database_backups_message_thread_id'); break; - case 'App\Notifications\ScheduledTask\TaskFailed': + case \App\Notifications\ScheduledTask\TaskFailed::class: $topicId = data_get($notifiable, 'telegram_notifications_scheduled_tasks_thread_id'); break; } if (! $telegramToken || ! $chatId || ! $message) { return; } - dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId)); + dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId))->onQueue('high'); } } diff --git a/app/Notifications/Channels/TransactionalEmailChannel.php b/app/Notifications/Channels/TransactionalEmailChannel.php index 3d7b7c8d0..cc7d76ebf 100644 --- a/app/Notifications/Channels/TransactionalEmailChannel.php +++ b/app/Notifications/Channels/TransactionalEmailChannel.php @@ -2,7 +2,6 @@ namespace App\Notifications\Channels; -use App\Models\InstanceSettings; use App\Models\User; use Exception; use Illuminate\Mail\Message; @@ -14,7 +13,7 @@ class TransactionalEmailChannel { public function send(User $notifiable, Notification $notification): void { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); if (! data_get($settings, 'smtp_enabled') && ! data_get($settings, 'resend_enabled')) { Log::info('SMTP/Resend not enabled'); diff --git a/app/Notifications/Container/ContainerRestarted.php b/app/Notifications/Container/ContainerRestarted.php index a55f16a83..182a1f5fc 100644 --- a/app/Notifications/Container/ContainerRestarted.php +++ b/app/Notifications/Container/ContainerRestarted.php @@ -3,6 +3,7 @@ namespace App\Notifications\Container; use App\Models\Server; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -14,9 +15,7 @@ class ContainerRestarted extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public string $name, public Server $server, public ?string $url = null) - { - } + public function __construct(public string $name, public Server $server, public ?string $url = null) {} public function via(object $notifiable): array { @@ -25,7 +24,7 @@ class ContainerRestarted extends Notification implements ShouldQueue public function toMail(): MailMessage { - $mail = new MailMessage(); + $mail = new MailMessage; $mail->subject("Coolify: A resource ({$this->name}) has been restarted automatically on {$this->server->name}"); $mail->view('emails.container-restarted', [ 'containerName' => $this->name, @@ -36,9 +35,17 @@ class ContainerRestarted extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = "Coolify: A resource ({$this->name}) has been restarted automatically on {$this->server->name}"; + $message = new DiscordMessage( + title: ':warning: Resource restarted', + description: "{$this->name} has been restarted automatically on {$this->server->name}.", + color: DiscordMessage::infoColor(), + ); + + if ($this->url) { + $message->addField('Resource', '[Link]('.$this->url.')'); + } return $message; } diff --git a/app/Notifications/Container/ContainerStopped.php b/app/Notifications/Container/ContainerStopped.php index d9dc57b98..33a55c65a 100644 --- a/app/Notifications/Container/ContainerStopped.php +++ b/app/Notifications/Container/ContainerStopped.php @@ -3,6 +3,7 @@ namespace App\Notifications\Container; use App\Models\Server; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -14,9 +15,7 @@ class ContainerStopped extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public string $name, public Server $server, public ?string $url = null) - { - } + public function __construct(public string $name, public Server $server, public ?string $url = null) {} public function via(object $notifiable): array { @@ -25,7 +24,7 @@ class ContainerStopped extends Notification implements ShouldQueue public function toMail(): MailMessage { - $mail = new MailMessage(); + $mail = new MailMessage; $mail->subject("Coolify: A resource has been stopped unexpectedly on {$this->server->name}"); $mail->view('emails.container-stopped', [ 'containerName' => $this->name, @@ -36,9 +35,17 @@ class ContainerStopped extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = "Coolify: A resource ($this->name) has been stopped unexpectedly on {$this->server->name}"; + $message = new DiscordMessage( + title: ':cross_mark: Resource stopped', + description: "{$this->name} has been stopped unexpectedly on {$this->server->name}.", + color: DiscordMessage::errorColor(), + ); + + if ($this->url) { + $message->addField('Resource', '[Link]('.$this->url.')'); + } return $message; } diff --git a/app/Notifications/Database/BackupFailed.php b/app/Notifications/Database/BackupFailed.php index c6403ab71..8e2733339 100644 --- a/app/Notifications/Database/BackupFailed.php +++ b/app/Notifications/Database/BackupFailed.php @@ -3,6 +3,7 @@ namespace App\Notifications\Database; use App\Models\ScheduledDatabaseBackup; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -33,7 +34,7 @@ class BackupFailed extends Notification implements ShouldQueue public function toMail(): MailMessage { - $mail = new MailMessage(); + $mail = new MailMessage; $mail->subject("Coolify: [ACTION REQUIRED] Backup FAILED for {$this->database->name}"); $mail->view('emails.backup-failed', [ 'name' => $this->name, @@ -45,9 +46,19 @@ class BackupFailed extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - return "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was FAILED.\n\nReason:\n{$this->output}"; + $message = new DiscordMessage( + title: ':cross_mark: Database backup failed', + description: "Database backup for {$this->name} (db:{$this->database_name}) has FAILED.", + color: DiscordMessage::errorColor(), + isCritical: true, + ); + + $message->addField('Frequency', $this->frequency, true); + $message->addField('Output', $this->output); + + return $message; } public function toTelegram(): array diff --git a/app/Notifications/Database/BackupSuccess.php b/app/Notifications/Database/BackupSuccess.php index f3a3d5943..5128c8ed6 100644 --- a/app/Notifications/Database/BackupSuccess.php +++ b/app/Notifications/Database/BackupSuccess.php @@ -3,6 +3,7 @@ namespace App\Notifications\Database; use App\Models\ScheduledDatabaseBackup; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -33,7 +34,7 @@ class BackupSuccess extends Notification implements ShouldQueue public function toMail(): MailMessage { - $mail = new MailMessage(); + $mail = new MailMessage; $mail->subject("Coolify: Backup successfully done for {$this->database->name}"); $mail->view('emails.backup-success', [ 'name' => $this->name, @@ -44,15 +45,22 @@ class BackupSuccess extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - return "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was successful."; + $message = new DiscordMessage( + title: ':white_check_mark: Database backup successful', + description: "Database backup for {$this->name} (db:{$this->database_name}) was successful.", + color: DiscordMessage::successColor(), + ); + + $message->addField('Frequency', $this->frequency, true); + + return $message; } public function toTelegram(): array { $message = "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was successful."; - ray($message); return [ 'message' => $message, diff --git a/app/Notifications/Database/DailyBackup.php b/app/Notifications/Database/DailyBackup.php deleted file mode 100644 index c74676eb7..000000000 --- a/app/Notifications/Database/DailyBackup.php +++ /dev/null @@ -1,52 +0,0 @@ -subject('Coolify: Daily backup statuses'); - $mail->view('emails.daily-backup', [ - 'databases' => $this->databases, - ]); - - return $mail; - } - - public function toDiscord(): string - { - return 'Coolify: Daily backup statuses'; - } - - public function toTelegram(): array - { - $message = 'Coolify: Daily backup statuses'; - - return [ - 'message' => $message, - ]; - } -} diff --git a/app/Notifications/Dto/DiscordMessage.php b/app/Notifications/Dto/DiscordMessage.php new file mode 100644 index 000000000..856753dca --- /dev/null +++ b/app/Notifications/Dto/DiscordMessage.php @@ -0,0 +1,83 @@ +fields[] = [ + 'name' => $name, + 'value' => $value, + 'inline' => $inline, + ]; + + return $this; + } + + public function toPayload(): array + { + $footerText = 'Coolify v'.config('version'); + if (isCloud()) { + $footerText = 'Coolify Cloud'; + } + $payload = [ + 'embeds' => [ + [ + 'title' => $this->title, + 'description' => $this->description, + 'color' => $this->color, + 'fields' => $this->addTimestampToFields($this->fields), + 'footer' => [ + 'text' => $footerText, + ], + ], + ], + ]; + if ($this->isCritical) { + $payload['content'] = '@here'; + } + + return $payload; + } + + private function addTimestampToFields(array $fields): array + { + $fields[] = [ + 'name' => 'Time', + 'value' => 'timestamp.':R>', + 'inline' => true, + ]; + + return $fields; + } +} diff --git a/app/Notifications/Internal/GeneralNotification.php b/app/Notifications/Internal/GeneralNotification.php index 6acd770f6..48e7d8340 100644 --- a/app/Notifications/Internal/GeneralNotification.php +++ b/app/Notifications/Internal/GeneralNotification.php @@ -4,6 +4,7 @@ namespace App\Notifications\Internal; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Notification; @@ -14,9 +15,7 @@ class GeneralNotification extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public string $message) - { - } + public function __construct(public string $message) {} public function via(object $notifiable): array { @@ -34,9 +33,13 @@ class GeneralNotification extends Notification implements ShouldQueue return $channels; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - return $this->message; + return new DiscordMessage( + title: 'Coolify: General Notification', + description: $this->message, + color: DiscordMessage::infoColor(), + ); } public function toTelegram(): array diff --git a/app/Notifications/ScheduledTask/TaskFailed.php b/app/Notifications/ScheduledTask/TaskFailed.php index 3a41fb687..c3501a8eb 100644 --- a/app/Notifications/ScheduledTask/TaskFailed.php +++ b/app/Notifications/ScheduledTask/TaskFailed.php @@ -3,6 +3,7 @@ namespace App\Notifications\ScheduledTask; use App\Models\ScheduledTask; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -29,13 +30,12 @@ class TaskFailed extends Notification implements ShouldQueue public function via(object $notifiable): array { - return setNotificationChannels($notifiable, 'scheduled_tasks'); } public function toMail(): MailMessage { - $mail = new MailMessage(); + $mail = new MailMessage; $mail->subject("Coolify: [ACTION REQUIRED] Scheduled task ({$this->task->name}) failed."); $mail->view('emails.scheduled-task-failed', [ 'task' => $this->task, @@ -46,9 +46,19 @@ class TaskFailed extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - return "Coolify: Scheduled task ({$this->task->name}, [link]({$this->url})) failed with output: {$this->output}"; + $message = new DiscordMessage( + title: ':cross_mark: Scheduled task failed', + description: "Scheduled task ({$this->task->name}) failed.", + color: DiscordMessage::errorColor(), + ); + + if ($this->url) { + $message->addField('Scheduled task', '[Link]('.$this->url.')'); + } + + return $message; } public function toTelegram(): array diff --git a/app/Notifications/Server/DockerCleanup.php b/app/Notifications/Server/DockerCleanup.php index 0e445f035..7ea1b84c2 100644 --- a/app/Notifications/Server/DockerCleanup.php +++ b/app/Notifications/Server/DockerCleanup.php @@ -5,6 +5,7 @@ namespace App\Notifications\Server; use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Notification; @@ -15,9 +16,7 @@ class DockerCleanup extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public Server $server, public string $message) - { - } + public function __construct(public Server $server, public string $message) {} public function via(object $notifiable): array { @@ -46,16 +45,18 @@ class DockerCleanup extends Notification implements ShouldQueue // $mail->view('emails.high-disk-usage', [ // 'name' => $this->server->name, // 'disk_usage' => $this->disk_usage, - // 'threshold' => $this->cleanup_after_percentage, + // 'threshold' => $this->docker_cleanup_threshold, // ]); // return $mail; // } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = "Coolify: Server '{$this->server->name}' cleanup job done!\n\n{$this->message}"; - - return $message; + return new DiscordMessage( + title: ':white_check_mark: Server cleanup job done', + description: $this->message, + color: DiscordMessage::successColor(), + ); } public function toTelegram(): array diff --git a/app/Notifications/Server/ForceDisabled.php b/app/Notifications/Server/ForceDisabled.php index 960a7c79f..a26c803ee 100644 --- a/app/Notifications/Server/ForceDisabled.php +++ b/app/Notifications/Server/ForceDisabled.php @@ -6,6 +6,7 @@ use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -17,9 +18,7 @@ class ForceDisabled extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public Server $server) - { - } + public function __construct(public Server $server) {} public function via(object $notifiable): array { @@ -43,7 +42,7 @@ class ForceDisabled extends Notification implements ShouldQueue public function toMail(): MailMessage { - $mail = new MailMessage(); + $mail = new MailMessage; $mail->subject("Coolify: Server ({$this->server->name}) disabled because it is not paid!"); $mail->view('emails.server-force-disabled', [ 'name' => $this->server->name, @@ -52,9 +51,15 @@ class ForceDisabled extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions)."; + $message = new DiscordMessage( + title: ':cross_mark: Server disabled', + description: "Server ({$this->server->name}) disabled because it is not paid!", + color: DiscordMessage::errorColor(), + ); + + $message->addField('Please update your subscription to enable the server again!', '[Link](https://app.coolify.io/subscriptions)'); return $message; } @@ -62,7 +67,7 @@ class ForceDisabled extends Notification implements ShouldQueue public function toTelegram(): array { return [ - 'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions).", + 'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).", ]; } } diff --git a/app/Notifications/Server/ForceEnabled.php b/app/Notifications/Server/ForceEnabled.php index 6a4b5d74b..65b65a10c 100644 --- a/app/Notifications/Server/ForceEnabled.php +++ b/app/Notifications/Server/ForceEnabled.php @@ -6,6 +6,7 @@ use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -17,9 +18,7 @@ class ForceEnabled extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public Server $server) - { - } + public function __construct(public Server $server) {} public function via(object $notifiable): array { @@ -43,7 +42,7 @@ class ForceEnabled extends Notification implements ShouldQueue public function toMail(): MailMessage { - $mail = new MailMessage(); + $mail = new MailMessage; $mail->subject("Coolify: Server ({$this->server->name}) enabled again!"); $mail->view('emails.server-force-enabled', [ 'name' => $this->server->name, @@ -52,11 +51,13 @@ class ForceEnabled extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = "Coolify: Server ({$this->server->name}) enabled again!"; - - return $message; + return new DiscordMessage( + title: ':white_check_mark: Server enabled', + description: "Server '{$this->server->name}' enabled again!", + color: DiscordMessage::successColor(), + ); } public function toTelegram(): array diff --git a/app/Notifications/Server/HighDiskUsage.php b/app/Notifications/Server/HighDiskUsage.php index 5f63ef8f1..e373abc03 100644 --- a/app/Notifications/Server/HighDiskUsage.php +++ b/app/Notifications/Server/HighDiskUsage.php @@ -3,9 +3,7 @@ namespace App\Notifications\Server; use App\Models\Server; -use App\Notifications\Channels\DiscordChannel; -use App\Notifications\Channels\EmailChannel; -use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -17,46 +15,39 @@ class HighDiskUsage extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public Server $server, public int $disk_usage, public int $cleanup_after_percentage) - { - } + public function __construct(public Server $server, public int $disk_usage, public int $server_disk_usage_notification_threshold) {} public function via(object $notifiable): array { - $channels = []; - $isEmailEnabled = isEmailEnabled($notifiable); - $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); - $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); - - if ($isDiscordEnabled) { - $channels[] = DiscordChannel::class; - } - if ($isEmailEnabled) { - $channels[] = EmailChannel::class; - } - if ($isTelegramEnabled) { - $channels[] = TelegramChannel::class; - } - - return $channels; + return setNotificationChannels($notifiable, 'server_disk_usage'); } public function toMail(): MailMessage { - $mail = new MailMessage(); + $mail = new MailMessage; $mail->subject("Coolify: Server ({$this->server->name}) high disk usage detected!"); $mail->view('emails.high-disk-usage', [ 'name' => $this->server->name, 'disk_usage' => $this->disk_usage, - 'threshold' => $this->cleanup_after_percentage, + 'threshold' => $this->server_disk_usage_notification_threshold, ]); return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->cleanup_after_percentage}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup."; + $message = new DiscordMessage( + title: ':cross_mark: High disk usage detected', + description: "Server '{$this->server->name}' high disk usage detected!", + color: DiscordMessage::errorColor(), + isCritical: true, + ); + + $message->addField('Disk usage', "{$this->disk_usage}%", true); + $message->addField('Threshold', "{$this->server_disk_usage_notification_threshold}%", true); + $message->addField('What to do?', '[Link](https://coolify.io/docs/knowledge-base/server/automated-cleanup)', true); + $message->addField('Change Settings', '[Threshold]('.base_url().'/server/'.$this->server->uuid.'#advanced) | [Notification]('.base_url().'/notifications/discord)'); return $message; } @@ -64,7 +55,7 @@ class HighDiskUsage extends Notification implements ShouldQueue public function toTelegram(): array { return [ - 'message' => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->cleanup_after_percentage}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.", + 'message' => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->server_disk_usage_notification_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.", ]; } } diff --git a/app/Notifications/Server/Revived.php b/app/Notifications/Server/Reachable.php similarity index 70% rename from app/Notifications/Server/Revived.php rename to app/Notifications/Server/Reachable.php index e7d3baf3e..9b54501d9 100644 --- a/app/Notifications/Server/Revived.php +++ b/app/Notifications/Server/Reachable.php @@ -2,34 +2,37 @@ namespace App\Notifications\Server; -use App\Actions\Docker\GetContainersStatus; -use App\Jobs\ContainerStatusJob; use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; -class Revived extends Notification implements ShouldQueue +class Reachable extends Notification implements ShouldQueue { use Queueable; public $tries = 1; + protected bool $isRateLimited = false; + public function __construct(public Server $server) { - if ($this->server->unreachable_notification_sent === false) { - return; - } - GetContainersStatus::dispatch($server); - // dispatch(new ContainerStatusJob($server)); + $this->isRateLimited = isEmailRateLimited( + limiterKey: 'server-reachable:'.$this->server->id, + ); } public function via(object $notifiable): array { + if ($this->isRateLimited) { + return []; + } + $channels = []; $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); @@ -50,7 +53,7 @@ class Revived extends Notification implements ShouldQueue public function toMail(): MailMessage { - $mail = new MailMessage(); + $mail = new MailMessage; $mail->subject("Coolify: Server ({$this->server->name}) revived."); $mail->view('emails.server-revived', [ 'name' => $this->server->name, @@ -59,11 +62,13 @@ class Revived extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!"; - - return $message; + return new DiscordMessage( + title: ":white_check_mark: Server '{$this->server->name}' revived", + description: 'All automations & integrations are turned on again!', + color: DiscordMessage::successColor(), + ); } public function toTelegram(): array diff --git a/app/Notifications/Server/Unreachable.php b/app/Notifications/Server/Unreachable.php index 2dcfe28b8..5bc568e82 100644 --- a/app/Notifications/Server/Unreachable.php +++ b/app/Notifications/Server/Unreachable.php @@ -6,6 +6,7 @@ use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -17,13 +18,21 @@ class Unreachable extends Notification implements ShouldQueue public $tries = 1; + protected bool $isRateLimited = false; + public function __construct(public Server $server) { - + $this->isRateLimited = isEmailRateLimited( + limiterKey: 'server-unreachable:'.$this->server->id, + ); } public function via(object $notifiable): array { + if ($this->isRateLimited) { + return []; + } + $channels = []; $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); @@ -42,9 +51,9 @@ class Unreachable extends Notification implements ShouldQueue return $channels; } - public function toMail(): MailMessage + public function toMail(): ?MailMessage { - $mail = new MailMessage(); + $mail = new MailMessage; $mail->subject("Coolify: Your server ({$this->server->name}) is unreachable."); $mail->view('emails.server-lost-connection', [ 'name' => $this->server->name, @@ -53,14 +62,20 @@ class Unreachable extends Notification implements ShouldQueue return $mail; } - public function toDiscord(): string + public function toDiscord(): ?DiscordMessage { - $message = "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations."; + $message = new DiscordMessage( + title: ':cross_mark: Server unreachable', + description: "Your server '{$this->server->name}' is unreachable.", + color: DiscordMessage::errorColor(), + ); + + $message->addField('IMPORTANT', 'We automatically try to revive your server and turn on all automations & integrations.'); return $message; } - public function toTelegram(): array + public function toTelegram(): ?array { return [ 'message' => "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations.", diff --git a/app/Notifications/Test.php b/app/Notifications/Test.php index 925859aba..a43b1e153 100644 --- a/app/Notifications/Test.php +++ b/app/Notifications/Test.php @@ -2,10 +2,12 @@ namespace App\Notifications; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; +use Illuminate\Queue\Middleware\RateLimited; class Test extends Notification implements ShouldQueue { @@ -13,29 +15,39 @@ class Test extends Notification implements ShouldQueue public $tries = 5; - public function __construct(public ?string $emails = null) - { - } + public function __construct(public ?string $emails = null) {} public function via(object $notifiable): array { return setNotificationChannels($notifiable, 'test'); } + public function middleware(object $notifiable, string $channel) + { + return match ($channel) { + \App\Notifications\Channels\EmailChannel::class => [new RateLimited('email')], + default => [], + }; + } + public function toMail(): MailMessage { - $mail = new MailMessage(); + $mail = new MailMessage; $mail->subject('Coolify: Test Email'); $mail->view('emails.test'); return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = 'Coolify: This is a test Discord notification from Coolify.'; - $message .= "\n\n"; - $message .= '[Go to your dashboard]('.base_url().')'; + $message = new DiscordMessage( + title: ':white_check_mark: Test Success', + description: 'This is a test Discord notification from Coolify. :cross_mark: :warning: :information_source:', + color: DiscordMessage::successColor(), + ); + + $message->addField(name: 'Dashboard', value: '[Link]('.base_url().')', inline: true); return $message; } diff --git a/app/Notifications/TransactionalEmails/InvitationLink.php b/app/Notifications/TransactionalEmails/InvitationLink.php index a251b47ea..6da2a6fcc 100644 --- a/app/Notifications/TransactionalEmails/InvitationLink.php +++ b/app/Notifications/TransactionalEmails/InvitationLink.php @@ -22,16 +22,14 @@ class InvitationLink extends Notification implements ShouldQueue return [TransactionalEmailChannel::class]; } - public function __construct(public User $user) - { - } + public function __construct(public User $user) {} public function toMail(): MailMessage { $invitation = TeamInvitation::whereEmail($this->user->email)->first(); $invitation_team = Team::find($invitation->team->id); - $mail = new MailMessage(); + $mail = new MailMessage; $mail->subject('Coolify: Invitation for '.$invitation_team->name); $mail->view('emails.invitation-link', [ 'team' => $invitation_team->name, diff --git a/app/Notifications/TransactionalEmails/ResetPassword.php b/app/Notifications/TransactionalEmails/ResetPassword.php index 45243c4d5..3938a8da7 100644 --- a/app/Notifications/TransactionalEmails/ResetPassword.php +++ b/app/Notifications/TransactionalEmails/ResetPassword.php @@ -18,7 +18,7 @@ class ResetPassword extends Notification public function __construct($token) { - $this->settings = InstanceSettings::get(); + $this->settings = instanceSettings(); $this->token = $token; } @@ -53,7 +53,7 @@ class ResetPassword extends Notification protected function buildMailMessage($url) { - $mail = new MailMessage(); + $mail = new MailMessage; $mail->subject('Coolify: Reset Password'); $mail->view('emails.reset-password', ['url' => $url, 'count' => config('auth.passwords.'.config('auth.defaults.passwords').'.expire')]); diff --git a/app/Notifications/TransactionalEmails/Test.php b/app/Notifications/TransactionalEmails/Test.php index ed30c1883..64883a58e 100644 --- a/app/Notifications/TransactionalEmails/Test.php +++ b/app/Notifications/TransactionalEmails/Test.php @@ -14,9 +14,7 @@ class Test extends Notification implements ShouldQueue public $tries = 5; - public function __construct(public string $emails) - { - } + public function __construct(public string $emails) {} public function via(): array { @@ -25,7 +23,7 @@ class Test extends Notification implements ShouldQueue public function toMail(): MailMessage { - $mail = new MailMessage(); + $mail = new MailMessage; $mail->subject('Coolify: Test Email'); $mail->view('emails.test'); diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 1bce22c12..015434bd2 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -5,17 +5,30 @@ namespace App\Providers; use App\Models\PersonalAccessToken; use Illuminate\Support\Facades\Http; use Illuminate\Support\ServiceProvider; +use Illuminate\Validation\Rules\Password; use Laravel\Sanctum\Sanctum; class AppServiceProvider extends ServiceProvider { public function register(): void { + if ($this->app->environment('local')) { + $this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class); + } } public function boot(): void { Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class); + + Password::defaults(function () { + $rule = Password::min(8); + + return $this->app->isProduction() + ? $rule->mixedCase()->letters()->numbers()->symbols() + : $rule; + }); + Http::macro('github', function (string $api_url, ?string $github_access_token = null) { if ($github_access_token) { return Http::withHeaders([ diff --git a/app/Providers/DuskServiceProvider.php b/app/Providers/DuskServiceProvider.php new file mode 100644 index 000000000..07e0e8709 --- /dev/null +++ b/app/Providers/DuskServiceProvider.php @@ -0,0 +1,21 @@ +visit('/login') + ->type('email', 'test@example.com') + ->type('password', 'password') + ->press('Login'); + }); + } +} diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index cd6ec7705..e8784bab3 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -6,7 +6,6 @@ use App\Actions\Fortify\CreateNewUser; use App\Actions\Fortify\ResetUserPassword; use App\Actions\Fortify\UpdateUserPassword; use App\Actions\Fortify\UpdateUserProfileInformation; -use App\Models\InstanceSettings; use App\Models\OauthSetting; use App\Models\User; use Illuminate\Cache\RateLimiting\Limit; @@ -45,19 +44,23 @@ class FortifyServiceProvider extends ServiceProvider { Fortify::createUsersUsing(CreateNewUser::class); Fortify::registerView(function () { - $settings = InstanceSettings::get(); + $isFirstUser = User::count() === 0; + + $settings = instanceSettings(); if (! $settings->is_registration_enabled) { return redirect()->route('login'); } if (config('coolify.waitlist')) { return redirect()->route('waitlist.index'); } else { - return view('auth.register'); + return view('auth.register', [ + 'isFirstUser' => $isFirstUser, + ]); } }); Fortify::loginView(function () { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $enabled_oauth_providers = OauthSetting::where('enabled', true)->get(); $users = User::count(); if ($users == 0) { @@ -72,7 +75,8 @@ class FortifyServiceProvider extends ServiceProvider }); Fortify::authenticateUsing(function (Request $request) { - $user = User::where('email', $request->email)->with('teams')->first(); + $email = strtolower($request->email); + $user = User::where('email', $email)->with('teams')->first(); if ( $user && Hash::check($request->password, $user->password) diff --git a/app/Providers/TelescopeServiceProvider.php b/app/Providers/TelescopeServiceProvider.php new file mode 100644 index 000000000..b7a336631 --- /dev/null +++ b/app/Providers/TelescopeServiceProvider.php @@ -0,0 +1,67 @@ +hideSensitiveRequestDetails(); + + $isLocal = $this->app->environment('local'); + + Telescope::filter(function (IncomingEntry $entry) use ($isLocal) { + return $isLocal || + $entry->isReportableException() || + $entry->isFailedRequest() || + $entry->isFailedJob() || + $entry->isScheduledTask() || + $entry->hasMonitoredTag(); + }); + } + + /** + * Prevent sensitive request details from being logged by Telescope. + */ + protected function hideSensitiveRequestDetails(): void + { + if ($this->app->environment('local')) { + return; + } + + Telescope::hideRequestParameters(['_token']); + + Telescope::hideRequestHeaders([ + 'cookie', + 'x-csrf-token', + 'x-xsrf-token', + ]); + } + + /** + * Register the Telescope gate. + * + * This gate determines who can access Telescope in non-local environments. + */ + protected function gate(): void + { + Gate::define('viewTelescope', function ($user) { + $root_user = User::find(0); + + return in_array($user->email, [ + $root_user->email, + ]); + }); + } +} diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index 0c6422f0c..f8ccee9db 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -3,11 +3,11 @@ namespace App\Traits; use App\Enums\ApplicationDeploymentStatus; +use App\Helpers\SshMultiplexingHelper; use App\Models\Server; use Carbon\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Process; -use Illuminate\Support\Str; trait ExecuteRemoteCommand { @@ -43,9 +43,9 @@ trait ExecuteRemoteCommand $command = parseLineForSudo($command, $this->server); } } - $remote_command = generateSshCommand($this->server, $command); + $remote_command = SshMultiplexingHelper::generateSshCommand($this->server, $command); $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append) { - $output = Str::of($output)->trim(); + $output = str($output)->trim(); if ($output->startsWith('╔')) { $output = "\n".$output; } diff --git a/app/View/Components/Forms/Datalist.php b/app/View/Components/Forms/Datalist.php index df0c1cb11..25643753d 100644 --- a/app/View/Components/Forms/Datalist.php +++ b/app/View/Components/Forms/Datalist.php @@ -30,7 +30,7 @@ class Datalist extends Component public function render(): View|Closure|string { if (is_null($this->id)) { - $this->id = new Cuid2(7); + $this->id = new Cuid2; } if (is_null($this->name)) { $this->name = $this->id; diff --git a/app/View/Components/Forms/Input.php b/app/View/Components/Forms/Input.php index 36c07dae1..7283ef20f 100644 --- a/app/View/Components/Forms/Input.php +++ b/app/View/Components/Forms/Input.php @@ -22,13 +22,15 @@ class Input extends Component public bool $allowToPeak = true, public bool $isMultiline = false, public string $defaultClass = 'input', - ) { - } + public string $autocomplete = 'off', + public ?int $minlength = null, + public ?int $maxlength = null, + ) {} public function render(): View|Closure|string { if (is_null($this->id)) { - $this->id = new Cuid2(7); + $this->id = new Cuid2; } if (is_null($this->name)) { $this->name = $this->id; diff --git a/app/View/Components/Forms/Select.php b/app/View/Components/Forms/Select.php index 21c147c2b..dd5ba66b7 100644 --- a/app/View/Components/Forms/Select.php +++ b/app/View/Components/Forms/Select.php @@ -30,7 +30,7 @@ class Select extends Component public function render(): View|Closure|string { if (is_null($this->id)) { - $this->id = new Cuid2(7); + $this->id = new Cuid2; } if (is_null($this->name)) { $this->name = $this->id; diff --git a/app/View/Components/Forms/Textarea.php b/app/View/Components/Forms/Textarea.php index bfdf03a31..6081c2a8a 100644 --- a/app/View/Components/Forms/Textarea.php +++ b/app/View/Components/Forms/Textarea.php @@ -19,6 +19,8 @@ class Textarea extends Component public ?string $value = null, public ?string $label = null, public ?string $placeholder = null, + public ?string $monacoEditorLanguage = '', + public bool $useMonacoEditor = false, public bool $required = false, public bool $disabled = false, public bool $readonly = false, @@ -28,7 +30,9 @@ class Textarea extends Component public bool $realtimeValidation = false, public bool $allowToPeak = true, public string $defaultClass = 'input scrollbar font-mono', - public string $defaultClassInput = 'input' + public string $defaultClassInput = 'input', + public ?int $minlength = null, + public ?int $maxlength = null, ) { // } @@ -39,7 +43,7 @@ class Textarea extends Component public function render(): View|Closure|string { if (is_null($this->id)) { - $this->id = new Cuid2(7); + $this->id = new Cuid2; } if (is_null($this->name)) { $this->name = $this->id; diff --git a/app/View/Components/ResourceView.php b/app/View/Components/ResourceView.php index 5a11b159d..d1107465b 100644 --- a/app/View/Components/ResourceView.php +++ b/app/View/Components/ResourceView.php @@ -16,9 +16,7 @@ class ResourceView extends Component public ?string $logo = null, public ?string $documentation = null, public bool $upgrade = false, - ) { - - } + ) {} /** * Get the view / contents that represent the component. diff --git a/app/View/Components/Server/Sidebar.php b/app/View/Components/Server/Sidebar.php deleted file mode 100644 index f968b6d0c..000000000 --- a/app/View/Components/Server/Sidebar.php +++ /dev/null @@ -1,27 +0,0 @@ -links = $this->links->merge($links); } else { if ($application->fqdn) { - $fqdns = collect(Str::of($application->fqdn)->explode(',')); + $fqdns = collect(str($application->fqdn)->explode(',')); $fqdns->map(function ($fqdn) { $this->links->push(getFqdnWithoutPort($fqdn)); }); } if ($application->ports) { - $portsCollection = collect(Str::of($application->ports)->explode(',')); + $portsCollection = collect(str($application->ports)->explode(',')); $portsCollection->map(function ($port) { - if (Str::of($port)->contains(':')) { - $hostPort = Str::of($port)->before(':'); + if (str($port)->contains(':')) { + $hostPort = str($port)->before(':'); } else { $hostPort = $port; } diff --git a/app/View/Components/Status/Index.php b/app/View/Components/Status/Index.php index f8436a102..ada9eb682 100644 --- a/app/View/Components/Status/Index.php +++ b/app/View/Components/Status/Index.php @@ -14,8 +14,7 @@ class Index extends Component public function __construct( public $resource = null, public bool $showRefreshButton = true, - ) { - } + ) {} /** * Get the view / contents that represent the component. diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index 999de45c2..875866e2f 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -1,12 +1,178 @@ user()->currentAccessToken(); return data_get($token, 'team_id'); } -function invalid_token() +function invalidTokenResponse() { - return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api-reference/authorization'], 400); + return response()->json(['message' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api-reference/authorization'], 400); +} + +function serializeApiResponse($data) +{ + if ($data instanceof Collection) { + return $data->map(function ($d) { + $d = collect($d)->sortKeys(); + $created_at = data_get($d, 'created_at'); + $updated_at = data_get($d, 'updated_at'); + if ($created_at) { + unset($d['created_at']); + $d['created_at'] = $created_at; + } + if ($updated_at) { + unset($d['updated_at']); + $d['updated_at'] = $updated_at; + } + if (data_get($d, 'name')) { + $d = $d->prepend($d['name'], 'name'); + } + if (data_get($d, 'description')) { + $d = $d->prepend($d['description'], 'description'); + } + if (data_get($d, 'uuid')) { + $d = $d->prepend($d['uuid'], 'uuid'); + } + + if (! is_null(data_get($d, 'id'))) { + $d = $d->prepend($d['id'], 'id'); + } + + return $d; + }); + } else { + $d = collect($data)->sortKeys(); + $created_at = data_get($d, 'created_at'); + $updated_at = data_get($d, 'updated_at'); + if ($created_at) { + unset($d['created_at']); + $d['created_at'] = $created_at; + } + if ($updated_at) { + unset($d['updated_at']); + $d['updated_at'] = $updated_at; + } + if (data_get($d, 'name')) { + $d = $d->prepend($d['name'], 'name'); + } + if (data_get($d, 'description')) { + $d = $d->prepend($d['description'], 'description'); + } + if (data_get($d, 'uuid')) { + $d = $d->prepend($d['uuid'], 'uuid'); + } + + if (! is_null(data_get($d, 'id'))) { + $d = $d->prepend($d['id'], 'id'); + } + + return $d; + } +} + +function sharedDataApplications() +{ + return [ + 'git_repository' => 'string', + 'git_branch' => 'string', + 'build_pack' => Rule::enum(BuildPackTypes::class), + 'is_static' => 'boolean', + 'static_image' => Rule::enum(StaticImageTypes::class), + 'domains' => 'string', + 'redirect' => Rule::enum(RedirectTypes::class), + 'git_commit_sha' => 'string', + 'docker_registry_image_name' => 'string|nullable', + 'docker_registry_image_tag' => 'string|nullable', + 'install_command' => 'string|nullable', + 'build_command' => 'string|nullable', + 'start_command' => 'string|nullable', + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/', + 'ports_mappings' => 'string|regex:/^(\d+:\d+)(,\d+:\d+)*$/|nullable', + 'base_directory' => 'string|nullable', + 'publish_directory' => 'string|nullable', + 'health_check_enabled' => 'boolean', + 'health_check_path' => 'string', + 'health_check_port' => 'string|nullable', + 'health_check_host' => 'string', + 'health_check_method' => 'string', + 'health_check_return_code' => 'numeric', + 'health_check_scheme' => 'string', + 'health_check_response_text' => 'string|nullable', + 'health_check_interval' => 'numeric', + 'health_check_timeout' => 'numeric', + 'health_check_retries' => 'numeric', + 'health_check_start_period' => 'numeric', + 'limits_memory' => 'string', + 'limits_memory_swap' => 'string', + 'limits_memory_swappiness' => 'numeric', + 'limits_memory_reservation' => 'string', + 'limits_cpus' => 'string', + 'limits_cpuset' => 'string|nullable', + 'limits_cpu_shares' => 'numeric', + 'custom_labels' => 'string|nullable', + 'custom_docker_run_options' => 'string|nullable', + 'post_deployment_command' => 'string|nullable', + 'post_deployment_command_container' => 'string', + 'pre_deployment_command' => 'string|nullable', + 'pre_deployment_command_container' => 'string', + 'manual_webhook_secret_github' => 'string|nullable', + 'manual_webhook_secret_gitlab' => 'string|nullable', + 'manual_webhook_secret_bitbucket' => 'string|nullable', + 'manual_webhook_secret_gitea' => 'string|nullable', + 'docker_compose_location' => 'string', + 'docker_compose' => 'string|nullable', + 'docker_compose_raw' => 'string|nullable', + 'docker_compose_domains' => 'array|nullable', + 'docker_compose_custom_start_command' => 'string|nullable', + 'docker_compose_custom_build_command' => 'string|nullable', + ]; +} + +function validateIncomingRequest(Request $request) +{ + // check if request is json + if (! $request->isJson()) { + return response()->json([ + 'message' => 'Invalid request.', + 'error' => 'Content-Type must be application/json.', + ], 400); + } + // check if request is valid json + if (! json_decode($request->getContent())) { + return response()->json([ + 'message' => 'Invalid request.', + 'error' => 'Invalid JSON.', + ], 400); + } + // check if valid json is empty + if (empty($request->json()->all())) { + return response()->json([ + 'message' => 'Invalid request.', + 'error' => 'Empty JSON.', + ], 400); + } +} + +function removeUnnecessaryFieldsFromRequest(Request $request) +{ + $request->offsetUnset('project_uuid'); + $request->offsetUnset('environment_name'); + $request->offsetUnset('destination_uuid'); + $request->offsetUnset('server_uuid'); + $request->offsetUnset('type'); + $request->offsetUnset('domains'); + $request->offsetUnset('instant_deploy'); + $request->offsetUnset('github_app_uuid'); + $request->offsetUnset('private_key_uuid'); + $request->offsetUnset('use_build_server'); + $request->offsetUnset('is_static'); } diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index 376b0f2aa..eb331f8c2 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -8,7 +8,7 @@ use App\Models\Server; use App\Models\StandaloneDocker; use Spatie\Url\Url; -function queue_application_deployment(Application $application, string $deployment_uuid, ?int $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, ?Server $server = null, ?StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false) +function queue_application_deployment(Application $application, string $deployment_uuid, ?int $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $is_api = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, ?Server $server = null, ?StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false) { $application_id = $application->id; $deployment_link = Url::fromString($application->link()."/deployment/{$deployment_uuid}"); @@ -35,6 +35,7 @@ function queue_application_deployment(Application $application, string $deployme 'pull_request_id' => $pull_request_id, 'force_rebuild' => $force_rebuild, 'is_webhook' => $is_webhook, + 'is_api' => $is_api, 'restart_only' => $restart_only, 'commit' => $commit, 'rollback' => $rollback, @@ -45,11 +46,11 @@ function queue_application_deployment(Application $application, string $deployme if ($no_questions_asked) { dispatch(new ApplicationDeploymentJob( application_deployment_queue_id: $deployment->id, - )); + ))->onQueue('high'); } elseif (next_queuable($server_id, $application_id)) { dispatch(new ApplicationDeploymentJob( application_deployment_queue_id: $deployment->id, - )); + ))->onQueue('high'); } } function force_start_deployment(ApplicationDeploymentQueue $deployment) @@ -60,12 +61,12 @@ function force_start_deployment(ApplicationDeploymentQueue $deployment) dispatch(new ApplicationDeploymentJob( application_deployment_queue_id: $deployment->id, - )); + ))->onQueue('high'); } function queue_next_deployment(Application $application) { $server_id = $application->destination->server_id; - $next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', 'queued')->get()->sortBy('created_at')->first(); + $next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', ApplicationDeploymentStatus::QUEUED)->get()->sortBy('created_at')->first(); if ($next_found) { $next_found->update([ 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, @@ -73,13 +74,13 @@ function queue_next_deployment(Application $application) dispatch(new ApplicationDeploymentJob( application_deployment_queue_id: $next_found->id, - )); + ))->onQueue('high'); } } function next_queuable(string $server_id, string $application_id): bool { - $deployments = ApplicationDeploymentQueue::where('server_id', $server_id)->whereIn('status', ['in_progress', 'queued'])->get()->sortByDesc('created_at'); + $deployments = ApplicationDeploymentQueue::where('server_id', $server_id)->whereIn('status', ['in_progress', ApplicationDeploymentStatus::QUEUED])->get()->sortByDesc('created_at'); $same_application_deployments = $deployments->where('application_id', $application_id); $in_progress = $same_application_deployments->filter(function ($value, $key) { return $value->status === 'in_progress'; @@ -90,7 +91,7 @@ function next_queuable(string $server_id, string $application_id): bool $server = Server::find($server_id); $concurrent_builds = $server->settings->concurrent_builds; - ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}"); + // ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}")->green(); if ($deployments->count() > $concurrent_builds) { return false; @@ -98,3 +99,26 @@ function next_queuable(string $server_id, string $application_id): bool return true; } +function next_after_cancel(?Server $server = null) +{ + if ($server) { + $next_found = ApplicationDeploymentQueue::where('server_id', data_get($server, 'id'))->where('status', ApplicationDeploymentStatus::QUEUED)->get()->sortBy('created_at'); + if ($next_found->count() > 0) { + foreach ($next_found as $next) { + $server = Server::find($next->server_id); + $concurrent_builds = $server->settings->concurrent_builds; + $inprogress_deployments = ApplicationDeploymentQueue::where('server_id', $next->server_id)->whereIn('status', [ApplicationDeploymentStatus::QUEUED])->get()->sortByDesc('created_at'); + if ($inprogress_deployments->count() < $concurrent_builds) { + $next->update([ + 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, + ]); + + dispatch(new ApplicationDeploymentJob( + application_deployment_queue_id: $next->id, + ))->onQueue('high'); + } + break; + } + } + } +} diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index e0272fa4c..303fcab8e 100644 --- a/bootstrap/helpers/constants.php +++ b/bootstrap/helpers/constants.php @@ -9,18 +9,27 @@ const VALID_CRON_STRINGS = [ 'weekly' => '0 0 * * 0', 'monthly' => '0 0 1 * *', 'yearly' => '0 0 1 1 *', + '@hourly' => '0 * * * *', + '@daily' => '0 0 * * *', + '@weekly' => '0 0 * * 0', + '@monthly' => '0 0 1 * *', + '@yearly' => '0 0 1 1 *', ]; const RESTART_MODE = 'unless-stopped'; const DATABASE_DOCKER_IMAGES = [ 'bitnami/mariadb', 'bitnami/mongodb', - 'bitnami/mysql', - 'bitnami/postgresql', 'bitnami/redis', 'mysql', + 'bitnami/mysql', + 'mysql/mysql-server', 'mariadb', + 'postgis/postgis', 'postgres', + 'bitnami/postgresql', + 'supabase/postgres', + 'elestio/postgres', 'mongo', 'redis', 'memcached', @@ -28,10 +37,10 @@ const DATABASE_DOCKER_IMAGES = [ 'neo4j', 'influxdb', 'clickhouse/clickhouse-server', - 'supabase/postgres', ]; const SPECIFIC_SERVICES = [ 'quay.io/minio/minio', + 'minio/minio', 'svhd/logto', ]; @@ -40,6 +49,8 @@ const SUPPORTED_OS = [ 'ubuntu debian raspbian', 'centos fedora rhel ol rocky amzn almalinux', 'sles opensuse-leap opensuse-tumbleweed', + 'arch', + 'alpine', ]; const SHARED_VARIABLE_TYPES = ['team', 'project', 'environment']; diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php index dba8aa543..e12910f82 100644 --- a/bootstrap/helpers/databases.php +++ b/bootstrap/helpers/databases.php @@ -1,5 +1,6 @@ first(); + $destination = StandaloneDocker::where('uuid', $destinationUuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandalonePostgresql; + $database->name = generate_database_name('postgresql'); + $database->image = $databaseImage; + $database->postgres_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environmentId; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandalonePostgresql::create([ - 'name' => generate_database_name('postgresql'), - 'postgres_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_redis($environment_id, $destination_uuid): StandaloneRedis +function create_standalone_redis($environment_id, $destination_uuid, ?array $otherData = null): StandaloneRedis { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneRedis; + $database->name = generate_database_name('redis'); + $redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneRedis::create([ - 'name' => generate_database_name('redis'), - 'redis_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), + EnvironmentVariable::create([ + 'key' => 'REDIS_PASSWORD', + 'value' => $redis_password, + 'standalone_redis_id' => $database->id, + 'is_shared' => false, ]); + + EnvironmentVariable::create([ + 'key' => 'REDIS_USERNAME', + 'value' => 'default', + 'standalone_redis_id' => $database->id, + 'is_shared' => false, + ]); + + return $database; } -function create_standalone_mongodb($environment_id, $destination_uuid): StandaloneMongodb +function create_standalone_mongodb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMongodb { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneMongodb; + $database->name = generate_database_name('mongodb'); + $database->mongo_initdb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneMongodb::create([ - 'name' => generate_database_name('mongodb'), - 'mongo_initdb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_mysql($environment_id, $destination_uuid): StandaloneMysql +function create_standalone_mysql($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMysql { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneMysql; + $database->name = generate_database_name('mysql'); + $database->mysql_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->mysql_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneMysql::create([ - 'name' => generate_database_name('mysql'), - 'mysql_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'mysql_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_mariadb($environment_id, $destination_uuid): StandaloneMariadb +function create_standalone_mariadb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMariadb { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneMariadb; + $database->name = generate_database_name('mariadb'); + $database->mariadb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->mariadb_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); - return StandaloneMariadb::create([ - 'name' => generate_database_name('mariadb'), - 'mariadb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'mariadb_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); + + return $database; } -function create_standalone_keydb($environment_id, $destination_uuid): StandaloneKeydb +function create_standalone_keydb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneKeydb { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneKeydb; + $database->name = generate_database_name('keydb'); + $database->keydb_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneKeydb::create([ - 'name' => generate_database_name('keydb'), - 'keydb_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_dragonfly($environment_id, $destination_uuid): StandaloneDragonfly +function create_standalone_dragonfly($environment_id, $destination_uuid, ?array $otherData = null): StandaloneDragonfly { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneDragonfly; + $database->name = generate_database_name('dragonfly'); + $database->dragonfly_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneDragonfly::create([ - 'name' => generate_database_name('dragonfly'), - 'dragonfly_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_clickhouse($environment_id, $destination_uuid): StandaloneClickhouse +function create_standalone_clickhouse($environment_id, $destination_uuid, ?array $otherData = null): StandaloneClickhouse { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneClickhouse; + $database->name = generate_database_name('clickhouse'); + $database->clickhouse_admin_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneClickhouse::create([ - 'name' => generate_database_name('clickhouse'), - 'clickhouse_admin_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -/** - * Delete file locally on the filesystem. - */ function delete_backup_locally(?string $filename, Server $server): void { if (empty($filename)) { @@ -156,3 +201,17 @@ function delete_backup_locally(?string $filename, Server $server): void } instant_remote_process(["rm -f \"{$filename}\""], $server, throwError: false); } + +function isPublicPortAlreadyUsed(Server $server, int $port, ?string $id = null): bool +{ + if ($id) { + $foundDatabase = $server->databases()->where('public_port', $port)->where('is_public', true)->where('id', '!=', $id)->first(); + } else { + $foundDatabase = $server->databases()->where('public_port', $port)->where('is_public', true)->first(); + } + if ($foundDatabase) { + return true; + } + + return false; +} diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 91e553cf6..40eacf5c8 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -1,5 +1,6 @@ filter(); - return $containers; + return $containers->filter(); + } + + return $containers; +} + +function getCurrentServiceContainerStatus(Server $server, int $id): Collection +{ + $containers = collect([]); + if (! $server->isSwarm()) { + $containers = instant_remote_process(["docker ps -a --filter='label=coolify.serviceId={$id}' --format '{{json .}}' "], $server); + $containers = format_docker_command_output_to_json($containers); + + return $containers->filter(); } return $containers; @@ -48,9 +61,13 @@ function format_docker_command_output_to_json($rawOutput): Collection $outputLines = collect($outputLines); } - return $outputLines - ->reject(fn ($line) => empty($line)) - ->map(fn ($outputLine) => json_decode($outputLine, true, flags: JSON_THROW_ON_ERROR)); + try { + return $outputLines + ->reject(fn ($line) => empty($line)) + ->map(fn ($outputLine) => json_decode($outputLine, true, flags: JSON_THROW_ON_ERROR)); + } catch (\Throwable) { + return collect([]); + } } function format_docker_labels_to_json(string|array $rawOutput): Collection @@ -85,13 +102,13 @@ function format_docker_envs_to_json($rawOutput) return [$env[0] => $env[1]]; }); - } catch (\Throwable $e) { + } catch (\Throwable) { return collect([]); } } function checkMinimumDockerEngineVersion($dockerVersion) { - $majorDockerVersion = Str::of($dockerVersion)->before('.')->value(); + $majorDockerVersion = str($dockerVersion)->before('.')->value(); if ($majorDockerVersion <= 22) { $dockerVersion = null; } @@ -115,6 +132,9 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data return 'exited'; } $container = format_docker_command_output_to_json($container); + if ($container->isEmpty()) { + return 'exited'; + } if ($all_data) { return $container[0]; } @@ -135,6 +155,8 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data function generateApplicationContainerName(Application $application, $pull_request_id = 0) { + // TODO: refactor generateApplicationContainerName, we do not need $application and $pull_request_id + $consistent_container_name = $application->settings->is_consistent_container_name_enabled; $now = now()->format('Hisu'); if ($pull_request_id !== 0 && $pull_request_id !== null) { @@ -152,7 +174,7 @@ function get_port_from_dockerfile($dockerfile): ?int $dockerfile_array = explode("\n", $dockerfile); $found_exposed_port = null; foreach ($dockerfile_array as $line) { - $line_str = Str::of($line)->trim(); + $line_str = str($line)->trim(); if ($line_str->startsWith('EXPOSE')) { $found_exposed_port = $line_str->replace('EXPOSE', '')->trim(); break; @@ -183,12 +205,12 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica } function generateServiceSpecificFqdns(ServiceApplication|Application $resource) { - if ($resource->getMorphClass() === 'App\Models\ServiceApplication') { + if ($resource->getMorphClass() === \App\Models\ServiceApplication::class) { $uuid = data_get($resource, 'uuid'); $server = data_get($resource, 'service.server'); $environment_variables = data_get($resource, 'service.environment_variables'); $type = $resource->serviceType(); - } elseif ($resource->getMorphClass() === 'App\Models\Application') { + } elseif ($resource->getMorphClass() === \App\Models\Application::class) { $uuid = data_get($resource, 'uuid'); $server = data_get($resource, 'destination.server'); $environment_variables = data_get($resource, 'environment_variables'); @@ -208,12 +230,12 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource) } if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) { $MINIO_BROWSER_REDIRECT_URL?->update([ - 'value' => generateFqdn($server, 'console-'.$uuid), + 'value' => generateFqdn($server, 'console-'.$uuid, true), ]); } if (is_null($MINIO_SERVER_URL?->value)) { $MINIO_SERVER_URL?->update([ - 'value' => generateFqdn($server, 'minio-'.$uuid), + 'value' => generateFqdn($server, 'minio-'.$uuid, true), ]); } $payload = collect([ @@ -246,7 +268,7 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource) return $payload; } -function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null, string $redirect_direction = 'both') +function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null, string $redirect_direction = 'both', ?string $predefinedPort = null) { $labels = collect([]); if ($serviceLabels) { @@ -255,7 +277,6 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, $labels->push("caddy_ingress_network={$network}"); } foreach ($domains as $loop => $domain) { - $loop = $loop; $url = Url::fromString($domain); $host = $url->getHost(); $path = $url->getPath(); @@ -265,6 +286,9 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, if (is_null($port) && ! is_null($onlyPort)) { $port = $onlyPort; } + if (is_null($port) && $predefinedPort) { + $port = $predefinedPort; + } $labels->push("caddy_{$loop}={$schema}://{$host}"); $labels->push("caddy_{$loop}.header=-Server"); $labels->push("caddy_{$loop}.try_files={path} /index.html /index.php"); @@ -298,43 +322,26 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels->push('traefik.http.middlewares.gzip.compress=true'); $labels->push('traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https'); - $basic_auth = false; - $basic_auth_middleware = null; - $redirect = false; - $redirect_middleware = null; + $middlewares_from_labels = collect([]); if ($serviceLabels) { - $basic_auth = $serviceLabels->contains(function ($value) { - return str_contains($value, 'basicauth'); - }); - if ($basic_auth) { - $basic_auth_middleware = $serviceLabels - ->map(function ($item) { - if (preg_match('/traefik\.http\.middlewares\.(.*?)\.basicauth\.users/', $item, $matches)) { - return $matches[1]; - } - }) - ->filter() - ->first(); - } - $redirect = $serviceLabels->contains(function ($value) { - return str_contains($value, 'redirectregex'); - }); - if ($redirect) { - $redirect_middleware = $serviceLabels - ->map(function ($item) { - if (preg_match('/traefik\.http\.middlewares\.(.*?)\.redirectregex\.regex/', $item, $matches)) { - return $matches[1]; - } - }) - ->filter() - ->first(); - } + $middlewares_from_labels = $serviceLabels->map(function ($item) { + if (preg_match('/traefik\.http\.middlewares\.(.*?)(\.|$)/', $item, $matches)) { + return $matches[1]; + } + if (preg_match('/coolify\.traefik\.middlewares=(.*)/', $item, $matches)) { + return explode(',', $matches[1]); + } + + return null; + })->flatten() + ->filter() + ->unique(); } foreach ($domains as $loop => $domain) { try { if ($generate_unique_uuid) { - $uuid = new Cuid2(7); + $uuid = new Cuid2; } $url = Url::fromString($domain); @@ -352,8 +359,11 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $https_label = "https-{$loop}-{$uuid}-{$service_name}"; } if (str($image)->contains('ghost')) { - $labels->push("traefik.http.middlewares.redir-ghost.redirectregex.regex=^{$path}/(.*)"); - $labels->push('traefik.http.middlewares.redir-ghost.redirectregex.replacement=/$1'); + $labels->push("traefik.http.middlewares.redir-ghost-{$uuid}.redirectregex.regex=^{$path}/(.*)"); + $labels->push("traefik.http.middlewares.redir-ghost-{$uuid}.redirectregex.replacement=/$1"); + $labels->push("caddy_{$loop}.handle_path.{$loop}_redir-ghost-{$uuid}.handler=rewrite"); + $labels->push("caddy_{$loop}.handle_path.{$loop}_redir-ghost-{$uuid}.rewrite.regexp=^{$path}/(.*)"); + $labels->push("caddy_{$loop}.handle_path.{$loop}_redir-ghost-{$uuid}.rewrite.replacement=/$1"); } $to_www_name = "{$loop}-{$uuid}-to-www"; @@ -377,6 +387,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels->push("traefik.http.services.{$https_label}.loadbalancer.server.port=$port"); } if ($path !== '/') { + // Middleware handling $middlewares = collect([]); if ($is_stripprefix_enabled && ! str($image)->contains('ghost')) { $labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}"); @@ -385,12 +396,6 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if ($is_gzip_enabled) { $middlewares->push('gzip'); } - if ($basic_auth && $basic_auth_middleware) { - $middlewares->push($basic_auth_middleware); - } - if ($redirect && $redirect_middleware) { - $middlewares->push($redirect_middleware); - } if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } @@ -402,6 +407,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels = $labels->merge($redirect_to_www); $middlewares->push($to_www_name); } + $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) { + $middlewares->push($middleware_name); + }); if ($middlewares->isNotEmpty()) { $middlewares = $middlewares->join(','); $labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}"); @@ -411,12 +419,6 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if ($is_gzip_enabled) { $middlewares->push('gzip'); } - if ($basic_auth && $basic_auth_middleware) { - $middlewares->push($basic_auth_middleware); - } - if ($redirect && $redirect_middleware) { - $middlewares->push($redirect_middleware); - } if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } @@ -428,6 +430,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels = $labels->merge($redirect_to_www); $middlewares->push($to_www_name); } + $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) { + $middlewares->push($middleware_name); + }); if ($middlewares->isNotEmpty()) { $middlewares = $middlewares->join(','); $labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}"); @@ -458,17 +463,11 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $middlewares = collect([]); if ($is_stripprefix_enabled && ! str($image)->contains('ghost')) { $labels->push("traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}"); - $middlewares->push("{$https_label}-stripprefix"); + $middlewares->push("{$http_label}-stripprefix"); } if ($is_gzip_enabled) { $middlewares->push('gzip'); } - if ($basic_auth && $basic_auth_middleware) { - $middlewares->push($basic_auth_middleware); - } - if ($redirect && $redirect_middleware) { - $middlewares->push($redirect_middleware); - } if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } @@ -480,6 +479,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels = $labels->merge($redirect_to_www); $middlewares->push($to_www_name); } + $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) { + $middlewares->push($middleware_name); + }); if ($middlewares->isNotEmpty()) { $middlewares = $middlewares->join(','); $labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}"); @@ -489,12 +491,6 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if ($is_gzip_enabled) { $middlewares->push('gzip'); } - if ($basic_auth && $basic_auth_middleware) { - $middlewares->push($basic_auth_middleware); - } - if ($redirect && $redirect_middleware) { - $middlewares->push($redirect_middleware); - } if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } @@ -506,13 +502,16 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels = $labels->merge($redirect_to_www); $middlewares->push($to_www_name); } + $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) { + $middlewares->push($middleware_name); + }); if ($middlewares->isNotEmpty()) { $middlewares = $middlewares->join(','); $labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}"); } } } - } catch (\Throwable $e) { + } catch (\Throwable) { continue; } } @@ -534,17 +533,96 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview $labels = collect([]); if ($pull_request_id === 0) { if ($application->fqdn) { - $domains = Str::of(data_get($application, 'fqdn'))->explode(','); + $domains = str(data_get($application, 'fqdn'))->explode(','); + $shouldGenerateLabelsExactly = $application->destination->server->settings->generate_exact_labels; + if ($shouldGenerateLabelsExactly) { + switch ($application->destination->server->proxyType()) { + case ProxyTypes::TRAEFIK->value: + $labels = $labels->merge(fqdnLabelsForTraefik( + uuid: $appUuid, + domains: $domains, + onlyPort: $onlyPort, + is_force_https_enabled: $application->isForceHttpsEnabled(), + is_gzip_enabled: $application->isGzipEnabled(), + is_stripprefix_enabled: $application->isStripprefixEnabled(), + redirect_direction: $application->redirect + )); + break; + case ProxyTypes::CADDY->value: + $labels = $labels->merge(fqdnLabelsForCaddy( + network: $application->destination->network, + uuid: $appUuid, + domains: $domains, + onlyPort: $onlyPort, + is_force_https_enabled: $application->isForceHttpsEnabled(), + is_gzip_enabled: $application->isGzipEnabled(), + is_stripprefix_enabled: $application->isStripprefixEnabled(), + redirect_direction: $application->redirect + )); + break; + } + } else { + $labels = $labels->merge(fqdnLabelsForTraefik( + uuid: $appUuid, + domains: $domains, + onlyPort: $onlyPort, + is_force_https_enabled: $application->isForceHttpsEnabled(), + is_gzip_enabled: $application->isGzipEnabled(), + is_stripprefix_enabled: $application->isStripprefixEnabled(), + redirect_direction: $application->redirect + )); + $labels = $labels->merge(fqdnLabelsForCaddy( + network: $application->destination->network, + uuid: $appUuid, + domains: $domains, + onlyPort: $onlyPort, + is_force_https_enabled: $application->isForceHttpsEnabled(), + is_gzip_enabled: $application->isGzipEnabled(), + is_stripprefix_enabled: $application->isStripprefixEnabled(), + redirect_direction: $application->redirect + )); + } + } + } else { + if (data_get($preview, 'fqdn')) { + $domains = str(data_get($preview, 'fqdn'))->explode(','); + } else { + $domains = collect([]); + } + $shouldGenerateLabelsExactly = $application->destination->server->settings->generate_exact_labels; + if ($shouldGenerateLabelsExactly) { + switch ($application->destination->server->proxyType()) { + case ProxyTypes::TRAEFIK->value: + $labels = $labels->merge(fqdnLabelsForTraefik( + uuid: $appUuid, + domains: $domains, + onlyPort: $onlyPort, + is_force_https_enabled: $application->isForceHttpsEnabled(), + is_gzip_enabled: $application->isGzipEnabled(), + is_stripprefix_enabled: $application->isStripprefixEnabled() + )); + break; + case ProxyTypes::CADDY->value: + $labels = $labels->merge(fqdnLabelsForCaddy( + network: $application->destination->network, + uuid: $appUuid, + domains: $domains, + onlyPort: $onlyPort, + is_force_https_enabled: $application->isForceHttpsEnabled(), + is_gzip_enabled: $application->isGzipEnabled(), + is_stripprefix_enabled: $application->isStripprefixEnabled() + )); + break; + } + } else { $labels = $labels->merge(fqdnLabelsForTraefik( uuid: $appUuid, domains: $domains, onlyPort: $onlyPort, is_force_https_enabled: $application->isForceHttpsEnabled(), is_gzip_enabled: $application->isGzipEnabled(), - is_stripprefix_enabled: $application->isStripprefixEnabled(), - redirect_direction: $application->redirect + is_stripprefix_enabled: $application->isStripprefixEnabled() )); - // Add Caddy labels $labels = $labels->merge(fqdnLabelsForCaddy( network: $application->destination->network, uuid: $appUuid, @@ -552,35 +630,9 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview onlyPort: $onlyPort, is_force_https_enabled: $application->isForceHttpsEnabled(), is_gzip_enabled: $application->isGzipEnabled(), - is_stripprefix_enabled: $application->isStripprefixEnabled(), - redirect_direction: $application->redirect + is_stripprefix_enabled: $application->isStripprefixEnabled() )); } - } else { - if (data_get($preview, 'fqdn')) { - $domains = Str::of(data_get($preview, 'fqdn'))->explode(','); - } else { - $domains = collect([]); - } - $labels = $labels->merge(fqdnLabelsForTraefik( - uuid: $appUuid, - domains: $domains, - onlyPort: $onlyPort, - is_force_https_enabled: $application->isForceHttpsEnabled(), - is_gzip_enabled: $application->isGzipEnabled(), - is_stripprefix_enabled: $application->isStripprefixEnabled() - )); - // Add Caddy labels - $labels = $labels->merge(fqdnLabelsForCaddy( - network: $application->destination->network, - uuid: $appUuid, - domains: $domains, - onlyPort: $onlyPort, - is_force_https_enabled: $application->isForceHttpsEnabled(), - is_gzip_enabled: $application->isGzipEnabled(), - is_stripprefix_enabled: $application->isStripprefixEnabled() - )); - } return $labels->all(); @@ -605,7 +657,7 @@ function isDatabaseImage(?string $image = null) return false; } -function convert_docker_run_to_compose(?string $custom_docker_run_options = null) +function convertDockerRunToCompose(?string $custom_docker_run_options = null) { $options = []; $compose_options = collect([]); @@ -617,21 +669,30 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null '--sysctl', '--ulimit', '--device', + '--shm-size', ]); $mapping = collect([ '--cap-add' => 'cap_add', '--cap-drop' => 'cap_drop', '--security-opt' => 'security_opt', '--sysctl' => 'sysctls', - '--ulimit' => 'ulimits', '--device' => 'devices', '--init' => 'init', '--ulimit' => 'ulimits', '--privileged' => 'privileged', '--ip' => 'ip', + '--shm-size' => 'shm_size', + '--gpus' => 'gpus', ]); foreach ($matches as $match) { $option = $match[1]; + if ($option === '--gpus') { + $regexForParsingDeviceIds = '/device=([0-9A-Za-z-,]+)/'; + preg_match($regexForParsingDeviceIds, $custom_docker_run_options, $device_matches); + $value = $device_matches[1] ?? 'all'; + $options[$option][] = $value; + $options[$option] = array_unique($options[$option]); + } if (isset($match[2]) && $match[2] !== '') { $value = $match[2]; $options[$option][] = $value; @@ -668,6 +729,32 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null } }); $compose_options->put($mapping[$option], $ulimits); + } elseif ($option === '--shm-size') { + if (! is_null($value) && is_array($value) && count($value) > 0) { + $compose_options->put($mapping[$option], $value[0]); + } + } elseif ($option === '--gpus') { + $payload = [ + 'driver' => 'nvidia', + 'capabilities' => ['gpu'], + ]; + if (! is_null($value) && is_array($value) && count($value) > 0) { + if (str($value[0]) != 'all') { + if (str($value[0])->contains(',')) { + $payload['device_ids'] = str($value[0])->explode(',')->toArray(); + } else { + $payload['device_ids'] = [$value[0]]; + } + } + } + ray($payload); + $compose_options->put('deploy', [ + 'resources' => [ + 'reservations' => [ + 'devices' => [$payload], + ], + ], + ]); } else { if ($list_options->contains($option)) { if ($compose_options->has($mapping[$option])) { @@ -689,6 +776,26 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null return $compose_options->toArray(); } +function generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $network) +{ + $ipv4 = data_get($docker_run_options, 'ip.0'); + $ipv6 = data_get($docker_run_options, 'ip6.0'); + data_forget($docker_run_options, 'ip'); + data_forget($docker_run_options, 'ip6'); + if ($ipv4 || $ipv6) { + data_forget($docker_compose['services'][$container_name], 'networks'); + } + if ($ipv4) { + $docker_compose['services'][$container_name]['networks'][$network]['ipv4_address'] = $ipv4; + } + if ($ipv6) { + $docker_compose['services'][$container_name]['networks'][$network]['ipv6_address'] = $ipv6; + } + $docker_compose['services'][$container_name] = array_merge_recursive($docker_compose['services'][$container_name], $docker_run_options); + + return $docker_compose; +} + function validateComposeFile(string $compose, int $server_id): string|Throwable { return 'OK'; diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php index d916dc9c8..529ac82b1 100644 --- a/bootstrap/helpers/github.php +++ b/bootstrap/helpers/github.php @@ -3,6 +3,7 @@ use App\Models\GithubApp; use App\Models\GitlabApp; use Carbon\Carbon; +use Carbon\CarbonImmutable; use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; use Lcobucci\JWT\Encoding\ChainedFormatter; @@ -14,9 +15,9 @@ use Lcobucci\JWT\Token\Builder; function generate_github_installation_token(GithubApp $source) { $signingKey = InMemory::plainText($source->privateKey->private_key); - $algorithm = new Sha256(); - $tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default())); - $now = new DateTimeImmutable(); + $algorithm = new Sha256; + $tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default())); + $now = CarbonImmutable::now(); $now = $now->setTime($now->format('H'), $now->format('i')); $issuedToken = $tokenBuilder ->issuedBy($source->app_id) @@ -38,18 +39,17 @@ function generate_github_installation_token(GithubApp $source) function generate_github_jwt_token(GithubApp $source) { $signingKey = InMemory::plainText($source->privateKey->private_key); - $algorithm = new Sha256(); - $tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default())); - $now = new DateTimeImmutable(); + $algorithm = new Sha256; + $tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default())); + $now = CarbonImmutable::now(); $now = $now->setTime($now->format('H'), $now->format('i')); - $issuedToken = $tokenBuilder + + return $tokenBuilder ->issuedBy($source->app_id) ->issuedAt($now->modify('-1 minute')) ->expiresAt($now->modify('+10 minutes')) ->getToken($algorithm, $signingKey) ->toString(); - - return $issuedToken; } function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $method = 'get', ?array $data = null, bool $throwError = true) @@ -57,7 +57,7 @@ function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $m if (is_null($source)) { throw new \Exception('Not implemented yet.'); } - if ($source->getMorphClass() == 'App\Models\GithubApp') { + if ($source->getMorphClass() === \App\Models\GithubApp::class) { if ($source->is_public) { $response = Http::github($source->api_url)->$method($endpoint); } else { @@ -85,7 +85,7 @@ function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $m function get_installation_path(GithubApp $source) { $github = GithubApp::where('uuid', $source->uuid)->first(); - $name = Str::of(Str::kebab($github->name)); + $name = str(Str::kebab($github->name)); $installation_path = $github->html_url === 'https://github.com' ? 'apps' : 'github-apps'; return "$github->html_url/$installation_path/$name/installations/new"; @@ -93,7 +93,7 @@ function get_installation_path(GithubApp $source) function get_permissions_path(GithubApp $source) { $github = GithubApp::where('uuid', $source->uuid)->first(); - $name = Str::of(Str::kebab($github->name)); + $name = str(Str::kebab($github->name)); return "$github->html_url/settings/apps/$name/permissions"; } diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index 2bf230c20..a8ef0fe5a 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -1,12 +1,29 @@ isFunctional()) { + return collect(); + } + $proxyType = $server->proxyType(); + if (is_null($proxyType) || $proxyType === 'NONE') { + return collect(); + } + $networks = instant_remote_process(['docker inspect --format="{{json .NetworkSettings.Networks }}" coolify-proxy'], $server, false); + + return collect($networks)->map(function ($network) { + return collect(json_decode($network))->keys(); + })->flatten()->unique(); +} +function collectDockerNetworksByServer(Server $server) +{ + $allNetworks = collect([]); if ($server->isSwarm()) { $networks = collect($server->swarmDockers)->map(function ($docker) { return $docker['network']; @@ -17,18 +34,28 @@ function connectProxyToNetworks(Server $server) return $docker['network']; }); } + $allNetworks = $allNetworks->merge($networks); // Service networks foreach ($server->services()->get() as $service) { - $networks->push($service->networks()); + if ($service->isRunning()) { + $networks->push($service->networks()); + } + $allNetworks->push($service->networks()); } // Docker compose based apps $docker_compose_apps = $server->dockerComposeBasedApplications(); foreach ($docker_compose_apps as $app) { - $networks->push($app->uuid); + if ($app->isRunning()) { + $networks->push($app->uuid); + } + $allNetworks->push($app->uuid); } // Docker compose based preview deployments $docker_compose_previews = $server->dockerComposeBasedPreviewDeployments(); foreach ($docker_compose_previews as $preview) { + if (! $preview->isRunning()) { + continue; + } $pullRequestId = $preview->pull_request_id; $applicationId = $preview->application_id; $application = Application::find($applicationId); @@ -37,28 +64,48 @@ function connectProxyToNetworks(Server $server) } $network = "{$application->uuid}-{$pullRequestId}"; $networks->push($network); + $allNetworks->push($network); } $networks = collect($networks)->flatten()->unique(); + $allNetworks = $allNetworks->flatten()->unique(); if ($server->isSwarm()) { if ($networks->count() === 0) { $networks = collect(['coolify-overlay']); + $allNetworks = collect(['coolify-overlay']); } + } else { + if ($networks->count() === 0) { + $networks = collect(['coolify']); + $allNetworks = collect(['coolify']); + } + } + + return [ + 'networks' => $networks, + 'allNetworks' => $allNetworks, + ]; +} +function connectProxyToNetworks(Server $server) +{ + ['networks' => $networks] = collectDockerNetworksByServer($server); + if ($server->isSwarm()) { $commands = $networks->map(function ($network) { return [ "echo 'Connecting coolify-proxy to $network network...'", "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --driver overlay --attachable $network >/dev/null", "docker network connect $network coolify-proxy >/dev/null 2>&1 || true", + "echo 'Successfully connected coolify-proxy to $network network.'", + "echo 'Proxy started and configured successfully!'", ]; }); } else { - if ($networks->count() === 0) { - $networks = collect(['coolify']); - } $commands = $networks->map(function ($network) { return [ "echo 'Connecting coolify-proxy to $network network...'", "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null", "docker network connect $network coolify-proxy >/dev/null 2>&1 || true", + "echo 'Successfully connected coolify-proxy to $network network.'", + "echo 'Proxy started and configured successfully!'", ]; }); } @@ -92,21 +139,21 @@ function generate_default_proxy_configuration(Server $server) 'external' => true, ]; }); - if ($proxy_type === 'TRAEFIK_V2') { + if ($proxy_type === ProxyTypes::TRAEFIK->value) { $labels = [ 'traefik.enable=true', 'traefik.http.routers.traefik.entrypoints=http', 'traefik.http.routers.traefik.service=api@internal', 'traefik.http.services.traefik.loadbalancer.server.port=8080', 'coolify.managed=true', + 'coolify.proxy=true', ]; $config = [ - 'version' => '3.8', 'networks' => $array_of_networks->toArray(), 'services' => [ 'traefik' => [ 'container_name' => 'coolify-proxy', - 'image' => 'traefik:v2.10', + 'image' => 'traefik:v3.1', 'restart' => RESTART_MODE, 'extra_hosts' => [ 'host.docker.internal:host-gateway', @@ -115,6 +162,7 @@ function generate_default_proxy_configuration(Server $server) 'ports' => [ '80:80', '443:443', + '443:443/udp', '8080:8080', ], 'healthcheck' => [ @@ -138,6 +186,7 @@ function generate_default_proxy_configuration(Server $server) '--entryPoints.http.http2.maxConcurrentStreams=50', '--entrypoints.https.http.encodequerysemicolons=true', '--entryPoints.https.http2.maxConcurrentStreams=50', + '--entrypoints.https.http3', '--providers.docker.exposedbydefault=false', '--providers.file.directory=/traefik/dynamic/', '--providers.file.watch=true', @@ -173,7 +222,6 @@ function generate_default_proxy_configuration(Server $server) } } elseif ($proxy_type === 'CADDY') { $config = [ - 'version' => '3.8', 'networks' => $array_of_networks->toArray(), 'services' => [ 'caddy' => [ @@ -191,13 +239,12 @@ function generate_default_proxy_configuration(Server $server) 'ports' => [ '80:80', '443:443', + '443:443/udp', + ], + 'labels' => [ + 'coolify.managed=true', + 'coolify.proxy=true', ], - // "healthcheck" => [ - // "test" => "wget -qO- http://localhost:80|| exit 1", - // "interval" => "4s", - // "timeout" => "2s", - // "retries" => 5, - // ], 'volumes' => [ '/var/run/docker.sock:/var/run/docker.sock:ro', "{$proxy_path}/dynamic:/dynamic", diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 918aa74cc..c7dd2cb83 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -3,6 +3,7 @@ use App\Actions\CoolifyTask\PrepareCoolifyTask; use App\Data\CoolifyTaskArgs; use App\Enums\ActivityTypes; +use App\Helpers\SshMultiplexingHelper; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; use App\Models\PrivateKey; @@ -10,9 +11,8 @@ use App\Models\Server; use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Process; -use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Spatie\Activitylog\Contracts\Activity; @@ -26,29 +26,28 @@ function remote_process( $callEventOnFinish = null, $callEventData = null ): Activity { - if (is_null($type)) { - $type = ActivityTypes::INLINE->value; - } - if ($command instanceof Collection) { - $command = $command->toArray(); - } + $type = $type ?? ActivityTypes::INLINE->value; + $command = $command instanceof Collection ? $command->toArray() : $command; + if ($server->isNonRoot()) { $command = parseCommandsByLineForSudo(collect($command), $server); } + $command_string = implode("\n", $command); - if (auth()->user()) { - $teams = auth()->user()->teams->pluck('id'); + + if (Auth::check()) { + $teams = Auth::user()->teams->pluck('id'); if (! $teams->contains($server->team_id) && ! $teams->contains(0)) { throw new \Exception('User is not part of the team that owns this server'); } } + SshMultiplexingHelper::ensureMultiplexedConnection($server); + return resolve(PrepareCoolifyTask::class, [ 'remoteProcessArgs' => new CoolifyTaskArgs( server_uuid: $server->uuid, - command: <<muxFilename(); - return [ - 'location' => $location, - 'mux_filename' => $mux_filename, - 'private_key_filename' => $private_key_filename, - ]; -} -function savePrivateKeyToFs(Server $server) -{ - if (data_get($server, 'privateKey.private_key') === null) { - throw new \Exception("Server {$server->name} does not have a private key"); - } - ['location' => $location, 'private_key_filename' => $private_key_filename] = server_ssh_configuration($server); - Storage::disk('ssh-keys')->makeDirectory('.'); - Storage::disk('ssh-mux')->makeDirectory('.'); - Storage::disk('ssh-keys')->put($private_key_filename, $server->privateKey->private_key); - - return $location; -} - -function generateScpCommand(Server $server, string $source, string $dest) -{ - $user = $server->user; - $port = $server->port; - $privateKeyLocation = savePrivateKeyToFs($server); - $timeout = config('constants.ssh.command_timeout'); - $connectionTimeout = config('constants.ssh.connection_timeout'); - $serverInterval = config('constants.ssh.server_interval'); - - $scp_command = "timeout $timeout scp "; - $scp_command .= "-i {$privateKeyLocation} " - .'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ' - .'-o PasswordAuthentication=no ' - ."-o ConnectTimeout=$connectionTimeout " - ."-o ServerAliveInterval=$serverInterval " - .'-o RequestTTY=no ' - .'-o LogLevel=ERROR ' - ."-P {$port} " - ."{$source} " - ."{$user}@{$server->ip}:{$dest}"; - - return $scp_command; -} function instant_scp(string $source, string $dest, Server $server, $throwError = true) { - $timeout = config('constants.ssh.command_timeout'); - $scp_command = generateScpCommand($server, $source, $dest); - $process = Process::timeout($timeout)->run($scp_command); + $scp_command = SshMultiplexingHelper::generateScpCommand($server, $source, $dest); + $process = Process::timeout(config('constants.ssh.command_timeout'))->run($scp_command); $output = trim($process->output()); $exitCode = $process->exitCode(); if ($exitCode !== 0) { - if (! $throwError) { - return null; - } - - return excludeCertainErrors($process->errorOutput(), $exitCode); - } - if ($output === 'null') { - $output = null; + return $throwError ? excludeCertainErrors($process->errorOutput(), $exitCode) : null; } - return $output; + return $output === 'null' ? null : $output; } -function generateSshCommand(Server $server, string $command) + +function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string { - if ($server->settings->force_disabled) { - throw new \RuntimeException('Server is disabled.'); - } - $user = $server->user; - $port = $server->port; - $privateKeyLocation = savePrivateKeyToFs($server); - $timeout = config('constants.ssh.command_timeout'); - $connectionTimeout = config('constants.ssh.connection_timeout'); - $serverInterval = config('constants.ssh.server_interval'); - $muxPersistTime = config('constants.ssh.mux_persist_time'); - - $ssh_command = "timeout $timeout ssh "; - - if (config('coolify.mux_enabled') && config('coolify.is_windows_docker_desktop') == false) { - $ssh_command .= "-o ControlMaster=auto -o ControlPersist={$muxPersistTime} -o ControlPath=/var/www/html/storage/app/ssh/mux/%h_%p_%r "; - } - if (data_get($server, 'settings.is_cloudflare_tunnel')) { - $ssh_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" '; - } - $command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command"; - $delimiter = Hash::make($command); - $command = str_replace($delimiter, '', $command); - $ssh_command .= "-i {$privateKeyLocation} " - .'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ' - .'-o PasswordAuthentication=no ' - ."-o ConnectTimeout=$connectionTimeout " - ."-o ServerAliveInterval=$serverInterval " - .'-o RequestTTY=no ' - .'-o LogLevel=ERROR ' - ."-p {$port} " - ."{$user}@{$server->ip} " - ." 'bash -se' << \\$delimiter".PHP_EOL - .$command.PHP_EOL - .$delimiter; - - // ray($ssh_command); - return $ssh_command; -} -function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false) -{ - $timeout = config('constants.ssh.command_timeout'); - if ($command instanceof Collection) { - $command = $command->toArray(); - } + $command = $command instanceof Collection ? $command->toArray() : $command; if ($server->isNonRoot() && ! $no_sudo) { $command = parseCommandsByLineForSudo(collect($command), $server); } $command_string = implode("\n", $command); - $ssh_command = generateSshCommand($server, $command_string, $no_sudo); - $process = Process::timeout($timeout)->run($ssh_command); + + // $start_time = microtime(true); + $sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string); + $process = Process::timeout(config('constants.ssh.command_timeout'))->run($sshCommand); + // $end_time = microtime(true); + + // $execution_time = ($end_time - $start_time) * 1000; // Convert to milliseconds + // ray('SSH command execution time:', $execution_time.' ms')->orange(); + $output = trim($process->output()); $exitCode = $process->exitCode(); + if ($exitCode !== 0) { - if (! $throwError) { - return null; - } - - return excludeCertainErrors($process->errorOutput(), $exitCode); - } - if ($output === 'null') { - $output = null; + return $throwError ? excludeCertainErrors($process->errorOutput(), $exitCode) : null; } - return $output; + return $output === 'null' ? null : $output; } + function excludeCertainErrors(string $errorOutput, ?int $exitCode = null) { $ignoredErrors = collect([ 'Permission denied (publickey', 'Could not resolve hostname', ]); - $ignored = false; - foreach ($ignoredErrors as $ignoredError) { - if (Str::contains($errorOutput, $ignoredError)) { - $ignored = true; - break; - } - } + $ignored = $ignoredErrors->contains(fn ($error) => Str::contains($errorOutput, $error)); if ($ignored) { // TODO: Create new exception and disable in sentry throw new \RuntimeException($errorOutput, $exitCode); } throw new \RuntimeException($errorOutput, $exitCode); } + function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null): Collection { - $application = Application::find(data_get($application_deployment_queue, 'application_id')); - $is_debug_enabled = data_get($application, 'settings.is_debug_enabled'); if (is_null($application_deployment_queue)) { return collect([]); } - // ray(data_get($application_deployment_queue, 'logs')); + $application = Application::find(data_get($application_deployment_queue, 'application_id')); + $is_debug_enabled = data_get($application, 'settings.is_debug_enabled'); try { $decoded = json_decode( data_get($application_deployment_queue, 'logs'), associative: true, flags: JSON_THROW_ON_ERROR ); - } catch (\JsonException $exception) { + } catch (\JsonException) { return collect([]); } - // ray($decoded ); + $seenCommands = collect(); $formatted = collect($decoded); if (! $is_debug_enabled) { $formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false); } - $formatted = $formatted + + return $formatted ->sortBy(fn ($i) => data_get($i, 'order')) ->map(function ($i) { data_set($i, 'timestamp', Carbon::parse(data_get($i, 'timestamp'))->format('Y-M-d H:i:s.u')); return $i; - }); + }) + ->reduce(function ($deploymentLogLines, $logItem) use ($seenCommands) { + $command = data_get($logItem, 'command'); + $isStderr = data_get($logItem, 'type') === 'stderr'; + $isNewCommand = ! is_null($command) && ! $seenCommands->first(function ($seenCommand) use ($logItem) { + return data_get($seenCommand, 'command') === data_get($logItem, 'command') && data_get($seenCommand, 'batch') === data_get($logItem, 'batch'); + }); - return $formatted; + if ($isNewCommand) { + $deploymentLogLines->push([ + 'line' => $command, + 'timestamp' => data_get($logItem, 'timestamp'), + 'stderr' => $isStderr, + 'hidden' => data_get($logItem, 'hidden'), + 'command' => true, + ]); + + $seenCommands->push([ + 'command' => $command, + 'batch' => data_get($logItem, 'batch'), + ]); + } + + $lines = explode(PHP_EOL, data_get($logItem, 'output')); + + foreach ($lines as $line) { + $deploymentLogLines->push([ + 'line' => $line, + 'timestamp' => data_get($logItem, 'timestamp'), + 'stderr' => $isStderr, + 'hidden' => data_get($logItem, 'hidden'), + ]); + } + + return $deploymentLogLines; + }, collect()); } + function remove_iip($text) { $text = preg_replace('/x-access-token:.*?(?=@)/', 'x-access-token:'.REDACTED, $text); return preg_replace('/\x1b\[[0-9;]*m/', '', $text); } -function remove_mux_and_private_key(Server $server) -{ - $muxFilename = $server->muxFilename(); - $privateKeyLocation = savePrivateKeyToFs($server); - Storage::disk('ssh-mux')->delete($muxFilename); - Storage::disk('ssh-keys')->delete($privateKeyLocation); -} + function refresh_server_connection(?PrivateKey $private_key = null) { if (is_null($private_key)) { return; } foreach ($private_key->servers as $server) { - Storage::disk('ssh-mux')->delete($server->muxFilename()); + SshMultiplexingHelper::removeMuxFile($server); } } @@ -277,24 +200,16 @@ function checkRequiredCommands(Server $server) foreach ($commands as $command) { $commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false); if ($commandFound) { - ray($command.' found'); - continue; } try { instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'apt update && apt install -y {$command}'"], $server); - } catch (\Throwable $e) { - ray('could not install '.$command); - ray($e); + } catch (\Throwable) { break; } $commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false); - if ($commandFound) { - ray($command.' found'); - - continue; + if (! $commandFound) { + break; } - ray('could not install '.$command); - break; } } diff --git a/bootstrap/helpers/s3.php b/bootstrap/helpers/s3.php index 4a2252016..2ee7bf44a 100644 --- a/bootstrap/helpers/s3.php +++ b/bootstrap/helpers/s3.php @@ -1,14 +1,11 @@ endpoint) { - $is_digital_ocean = Str::contains($s3->endpoint, 'digitaloceanspaces.com'); - } + config()->set('filesystems.disks.custom-s3', [ 'driver' => 's3', 'region' => $s3['region'], @@ -17,7 +14,7 @@ function set_s3_target(S3Storage $s3) 'bucket' => $s3['bucket'], 'endpoint' => $s3['endpoint'], 'use_path_style_endpoint' => true, - 'bucket_endpoint' => $is_digital_ocean, + 'bucket_endpoint' => $s3->isHetzner() || $s3->isDigitalOcean(), 'aws_url' => $s3->awsUrl(), ]); } diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index 0cc4c51e7..fd2e1231f 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -4,7 +4,7 @@ use App\Models\Application; use App\Models\EnvironmentVariable; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; -use Illuminate\Support\Str; +use Illuminate\Support\Stringable; use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; @@ -16,15 +16,15 @@ function collectRegex(string $name) { return "/{$name}\w+/"; } -function replaceVariables($variable) +function replaceVariables(string $variable): Stringable { - return $variable->before('}')->replaceFirst('$', '')->replaceFirst('{', ''); + return str($variable)->before('}')->replaceFirst('$', '')->replaceFirst('{', ''); } function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Application $oneService, bool $isInit = false) { try { - if ($oneService->getMorphClass() === 'App\Models\Application') { + if ($oneService->getMorphClass() === \App\Models\Application::class) { $workdir = $oneService->workdir(); $server = $oneService->destination->server; } else { @@ -38,7 +38,7 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli ]); instant_remote_process($commands, $server); foreach ($fileVolumes as $fileVolume) { - $path = Str::of(data_get($fileVolume, 'fs_path')); + $path = str(data_get($fileVolume, 'fs_path')); $content = data_get($fileVolume, 'content'); if ($path->startsWith('.')) { $path = $path->after('.'); @@ -51,29 +51,38 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli // Exists and is a directory $isDir = instant_remote_process(["test -d $fileLocation && echo OK || echo NOK"], $server); - if ($isFile == 'OK') { + if ($isFile === 'OK') { // If its a file & exists $filesystemContent = instant_remote_process(["cat $fileLocation"], $server); - $fileVolume->content = $filesystemContent; + if ($fileVolume->is_based_on_git) { + $fileVolume->content = $filesystemContent; + } $fileVolume->is_directory = false; $fileVolume->save(); - } elseif ($isDir == 'OK') { + } elseif ($isDir === 'OK') { // If its a directory & exists $fileVolume->content = null; $fileVolume->is_directory = true; $fileVolume->save(); - } elseif ($isFile == 'NOK' && $isDir == 'NOK' && ! $fileVolume->is_directory && $isInit && $content) { + } elseif ($isFile === 'NOK' && $isDir === 'NOK' && ! $fileVolume->is_directory && $isInit && $content) { // Does not exists (no dir or file), not flagged as directory, is init, has content $fileVolume->content = $content; $fileVolume->is_directory = false; $fileVolume->save(); $content = base64_encode($content); - $dir = Str::of($fileLocation)->dirname(); + $dir = str($fileLocation)->dirname(); instant_remote_process([ "mkdir -p $dir", "echo '$content' | base64 -d | tee $fileLocation", ], $server); - } elseif ($isFile == 'NOK' && $isDir == 'NOK' && $fileVolume->is_directory && $isInit) { + } elseif ($isFile === 'NOK' && $isDir === 'NOK' && $fileVolume->is_directory && $isInit) { + // Does not exists (no dir or file), flagged as directory, is init + $fileVolume->content = null; + $fileVolume->is_directory = true; + $fileVolume->save(); + instant_remote_process(["mkdir -p $fileLocation"], $server); + } elseif ($isFile === 'NOK' && $isDir === 'NOK' && ! $fileVolume->is_directory && $isInit && is_null($content)) { + // Does not exists (no dir or file), not flagged as directory, is init, has no content => create directory $fileVolume->content = null; $fileVolume->is_directory = true; $fileVolume->save(); @@ -89,6 +98,9 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) try { $name = data_get($resource, 'name'); $dockerComposeRaw = data_get($resource, 'service.docker_compose_raw'); + if (! $dockerComposeRaw) { + throw new \Exception('No compose file found or not a valid YAML file.'); + } $dockerCompose = Yaml::parse($dockerComposeRaw); // Switch Image @@ -106,41 +118,56 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $resourceFqdns = str($resource->fqdn)->explode(','); if ($resourceFqdns->count() === 1) { $resourceFqdns = $resourceFqdns->first(); - $variableName = 'SERVICE_FQDN_'.Str::of($resource->name)->upper()->replace('-', ''); + $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', ''); $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $fqdn = Url::fromString($resourceFqdns); $port = $fqdn->getPort(); $path = $fqdn->getPath(); $fqdn = $fqdn->getScheme().'://'.$fqdn->getHost(); if ($generatedEnv) { - $generatedEnv->value = $fqdn.$path; + if ($path === '/') { + $generatedEnv->value = $fqdn; + } else { + $generatedEnv->value = $fqdn.$path; + } $generatedEnv->save(); } if ($port) { $variableName = $variableName."_$port"; $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); - // ray($generatedEnv); if ($generatedEnv) { - $generatedEnv->value = $fqdn.$path; + if ($path === '/') { + $generatedEnv->value = $fqdn; + } else { + $generatedEnv->value = $fqdn.$path; + } $generatedEnv->save(); } } - $variableName = 'SERVICE_URL_'.Str::of($resource->name)->upper()->replace('-', ''); + $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', ''); $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $url = Url::fromString($fqdn); $port = $url->getPort(); $path = $url->getPath(); $url = $url->getHost(); if ($generatedEnv) { - $url = Str::of($fqdn)->after('://'); - $generatedEnv->value = $url.$path; + $url = str($fqdn)->after('://'); + if ($path === '/') { + $generatedEnv->value = $url; + } else { + $generatedEnv->value = $url.$path; + } $generatedEnv->save(); } if ($port) { $variableName = $variableName."_$port"; $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); if ($generatedEnv) { - $generatedEnv->value = $url.$path; + if ($path === '/') { + $generatedEnv->value = $url; + } else { + $generatedEnv->value = $url.$path; + } $generatedEnv->save(); } } @@ -157,10 +184,18 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $service_fqdn = str($port_env->key)->beforeLast('_')->after('SERVICE_FQDN_'); $env = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'SERVICE_FQDN_'.$service_fqdn)->first(); if ($env) { - $env->value = $host.$path; + if ($path === '/') { + $env->value = $host; + } else { + $env->value = $host.$path; + } $env->save(); } - $port_env->value = $host.$path; + if ($path === '/') { + $port_env->value = $host; + } else { + $port_env->value = $host.$path; + } $port_env->save(); } $port_envs_url = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'like', "SERVICE_URL_%_$port")->get(); @@ -168,14 +203,22 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $service_url = str($port_env_url->key)->beforeLast('_')->after('SERVICE_URL_'); $env = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'SERVICE_URL_'.$service_url)->first(); if ($env) { - $env->value = $url.$path; + if ($path === '/') { + $env->value = $url; + } else { + $env->value = $url.$path; + } $env->save(); } - $port_env_url->value = $url.$path; + if ($path === '/') { + $port_env_url->value = $url; + } else { + $port_env_url->value = $url.$path; + } $port_env_url->save(); } } else { - $variableName = 'SERVICE_FQDN_'.Str::of($resource->name)->upper()->replace('-', ''); + $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', ''); $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $fqdn = Url::fromString($fqdn); $fqdn = $fqdn->getScheme().'://'.$fqdn->getHost().$fqdn->getPath(); @@ -183,12 +226,12 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $generatedEnv->value = $fqdn; $generatedEnv->save(); } - $variableName = 'SERVICE_URL_'.Str::of($resource->name)->upper()->replace('-', ''); + $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', ''); $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $url = Url::fromString($fqdn); $url = $url->getHost().$url->getPath(); if ($generatedEnv) { - $url = Str::of($fqdn)->after('://'); + $url = str($fqdn)->after('://'); $generatedEnv->value = $url; $generatedEnv->save(); } @@ -200,3 +243,7 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) return handleError($e); } } +function serviceKeys() +{ + return get_service_templates()->keys(); +} diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 7994c10af..6e52dcde9 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1,11 +1,13 @@ user()?->currentTeam() ?? null; + return Auth::user()?->currentTeam() ?? null; } function showBoarding(): bool { - if (auth()->user()?->isMember()) { + if (Auth::user()?->isMember()) { return false; } @@ -106,21 +116,20 @@ function showBoarding(): bool function refreshSession(?Team $team = null): void { if (! $team) { - if (auth()->user()?->currentTeam()) { - $team = Team::find(auth()->user()->currentTeam()->id); + if (Auth::user()->currentTeam()) { + $team = Team::find(Auth::user()->currentTeam()->id); } else { - $team = User::find(auth()->user()->id)->teams->first(); + $team = User::find(Auth::id())->teams->first(); } } - Cache::forget('team:'.auth()->user()->id); - Cache::remember('team:'.auth()->user()->id, 3600, function () use ($team) { + Cache::forget('team:'.Auth::id()); + Cache::remember('team:'.Auth::id(), 3600, function () use ($team) { return $team; }); session(['currentTeam' => $team]); } function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null) { - ray($error); if ($error instanceof TooManyRequestsException) { if (isset($livewire)) { return $livewire->dispatch('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds."); @@ -136,6 +145,10 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n return 'Duplicate entry found. Please use a different name.'; } + if ($error instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) { + abort(404); + } + if ($error instanceof Throwable) { $message = $error->getMessage(); } else { @@ -162,10 +175,7 @@ function get_latest_sentinel_version(): string $versions = $response->json(); return data_get($versions, 'coolify.sentinel.version'); - } catch (\Throwable $e) { - //throw $e; - ray($e->getMessage()); - + } catch (\Throwable) { return '0.0.0'; } } @@ -176,11 +186,7 @@ function get_latest_version_of_coolify(): string $versions = json_decode($versions, true); return data_get($versions, 'coolify.v4.version'); - // $response = Http::get('https://cdn.coollabs.io/coolify/versions.json'); - // $versions = $response->json(); - // return data_get($versions, 'coolify.v4.version'); } catch (\Throwable $e) { - //throw $e; ray($e->getMessage()); return '0.0.0'; @@ -191,11 +197,11 @@ function generate_random_name(?string $cuid = null): string { $generator = new \Nubs\RandomNameGenerator\All( [ - new \Nubs\RandomNameGenerator\Alliteration(), + new \Nubs\RandomNameGenerator\Alliteration, ] ); if (is_null($cuid)) { - $cuid = new Cuid2(7); + $cuid = new Cuid2; } return Str::kebab("{$generator->getName()}-$cuid"); @@ -231,7 +237,7 @@ function formatPrivateKey(string $privateKey) function generate_application_name(string $git_repository, string $git_branch, ?string $cuid = null): string { if (is_null($cuid)) { - $cuid = new Cuid2(7); + $cuid = new Cuid2; } return Str::kebab("$git_repository:$git_branch-$cuid"); @@ -239,13 +245,13 @@ function generate_application_name(string $git_repository, string $git_branch, ? function is_transactional_emails_active(): bool { - return isEmailEnabled(InstanceSettings::get()); + return isEmailEnabled(\App\Models\InstanceSettings::get()); } function set_transanctional_email_settings(?InstanceSettings $settings = null): ?string { if (! $settings) { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); } config()->set('mail.from.address', data_get($settings, 'smtp_from_address')); config()->set('mail.from.name', data_get($settings, 'smtp_from_name')); @@ -279,7 +285,7 @@ function base_ip(): string if (isDev()) { return 'localhost'; } - $settings = InstanceSettings::get(); + $settings = instanceSettings(); if ($settings->public_ipv4) { return "$settings->public_ipv4"; } @@ -298,7 +304,7 @@ function getFqdnWithoutPort(string $fqdn) $path = $url->getPath(); return "$scheme://$host$path"; - } catch (\Throwable $e) { + } catch (\Throwable) { return $fqdn; } } @@ -307,7 +313,7 @@ function getFqdnWithoutPort(string $fqdn) */ function base_url(bool $withPort = true): string { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); if ($settings->fqdn) { return $settings->fqdn; } @@ -341,6 +347,11 @@ function isSubscribed() { return isSubscriptionActive() || auth()->user()->isInstanceAdmin(); } + +function isProduction(): bool +{ + return ! isDev(); +} function isDev(): bool { return config('app.env') === 'local'; @@ -351,8 +362,19 @@ function isCloud(): bool return ! config('coolify.self_hosted'); } +function translate_cron_expression($expression_to_validate): string +{ + if (isset(VALID_CRON_STRINGS[$expression_to_validate])) { + return VALID_CRON_STRINGS[$expression_to_validate]; + } + + return $expression_to_validate; +} function validate_cron_expression($expression_to_validate): bool { + if (empty($expression_to_validate)) { + return false; + } $isValid = false; $expression = new CronExpression($expression_to_validate); $isValid = $expression->isValid(); @@ -374,7 +396,7 @@ function send_internal_notification(string $message): void } function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null): void { - $settings = InstanceSettings::get(); + $settings = instanceSettings(); $type = set_transanctional_email_settings($settings); if (! $type) { throw new Exception('No email settings found.'); @@ -465,10 +487,10 @@ function data_get_str($data, $key, $default = null): Stringable { $str = data_get($data, $key, $default) ?? $default; - return Str::of($str); + return str($str); } -function generateFqdn(Server $server, string $random) +function generateFqdn(Server $server, string $random, bool $forceHttps = false): string { $wildcard = data_get($server, 'settings.wildcard_domain'); if (is_null($wildcard) || $wildcard === '') { @@ -478,9 +500,11 @@ function generateFqdn(Server $server, string $random) $host = $url->getHost(); $path = $url->getPath() === '/' ? '' : $url->getPath(); $scheme = $url->getScheme(); - $finalFqdn = "$scheme://{$random}.$host$path"; + if ($forceHttps) { + $scheme = 'https'; + } - return $finalFqdn; + return "$scheme://{$random}.$host$path"; } function sslip(Server $server) { @@ -492,22 +516,33 @@ function sslip(Server $server) return "http://$baseIp.sslip.io"; } + // ipv6 + if (str($server->ip)->contains(':')) { + $ipv6 = str($server->ip)->replace(':', '-'); + + return "http://{$ipv6}.sslip.io"; + } return "http://{$server->ip}.sslip.io"; } function get_service_templates(bool $force = false): Collection { + if (isDev()) { + $services = File::get(base_path('templates/service-templates.json')); + + return collect(json_decode($services))->sortKeys(); + } if ($force) { try { - $response = Http::retry(3, 50)->get(config('constants.services.official')); + $response = Http::retry(3, 1000)->get(config('constants.services.official')); if ($response->failed()) { return collect([]); } $services = $response->json(); return collect($services); - } catch (\Throwable $e) { + } catch (\Throwable) { $services = File::get(base_path('templates/service-templates.json')); return collect(json_decode($services))->sortKeys(); @@ -531,6 +566,43 @@ function getResourceByUuid(string $uuid, ?int $teamId = null) return null; } +function queryDatabaseByUuidWithinTeam(string $uuid, string $teamId) +{ + $postgresql = StandalonePostgresql::whereUuid($uuid)->first(); + if ($postgresql && $postgresql->team()->id == $teamId) { + return $postgresql->unsetRelation('environment')->unsetRelation('destination'); + } + $redis = StandaloneRedis::whereUuid($uuid)->first(); + if ($redis && $redis->team()->id == $teamId) { + return $redis->unsetRelation('environment'); + } + $mongodb = StandaloneMongodb::whereUuid($uuid)->first(); + if ($mongodb && $mongodb->team()->id == $teamId) { + return $mongodb->unsetRelation('environment'); + } + $mysql = StandaloneMysql::whereUuid($uuid)->first(); + if ($mysql && $mysql->team()->id == $teamId) { + return $mysql->unsetRelation('environment'); + } + $mariadb = StandaloneMariadb::whereUuid($uuid)->first(); + if ($mariadb && $mariadb->team()->id == $teamId) { + return $mariadb->unsetRelation('environment'); + } + $keydb = StandaloneKeydb::whereUuid($uuid)->first(); + if ($keydb && $keydb->team()->id == $teamId) { + return $keydb->unsetRelation('environment'); + } + $dragonfly = StandaloneDragonfly::whereUuid($uuid)->first(); + if ($dragonfly && $dragonfly->team()->id == $teamId) { + return $dragonfly->unsetRelation('environment'); + } + $clickhouse = StandaloneClickhouse::whereUuid($uuid)->first(); + if ($clickhouse && $clickhouse->team()->id == $teamId) { + return $clickhouse->unsetRelation('environment'); + } + + return null; +} function queryResourcesByUuid(string $uuid) { $resource = null; @@ -577,14 +649,13 @@ function queryResourcesByUuid(string $uuid) return $resource; } -function generatTagDeployWebhook($tag_name) +function generateTagDeployWebhook($tag_name) { $baseUrl = base_url(); $api = Url::fromString($baseUrl).'/api/v1'; $endpoint = "/deploy?tag=$tag_name"; - $url = $api.$endpoint; - return $url; + return $api.$endpoint; } function generateDeployWebhook($resource) { @@ -592,20 +663,18 @@ function generateDeployWebhook($resource) $api = Url::fromString($baseUrl).'/api/v1'; $endpoint = '/deploy'; $uuid = data_get($resource, 'uuid'); - $url = $api.$endpoint."?uuid=$uuid&force=false"; - return $url; + return $api.$endpoint."?uuid=$uuid&force=false"; } function generateGitManualWebhook($resource, $type) { if ($resource->source_id !== 0 && ! is_null($resource->source_id)) { return null; } - if ($resource->getMorphClass() === 'App\Models\Application') { + if ($resource->getMorphClass() === \App\Models\Application::class) { $baseUrl = base_url(); - $api = Url::fromString($baseUrl)."/webhooks/source/$type/events/manual"; - return $api; + return Url::fromString($baseUrl)."/webhooks/source/$type/events/manual"; } return null; @@ -617,7 +686,7 @@ function removeAnsiColors($text) function getTopLevelNetworks(Service|Application $resource) { - if ($resource->getMorphClass() === 'App\Models\Service') { + if ($resource->getMorphClass() === \App\Models\Service::class) { if ($resource->docker_compose_raw) { try { $yaml = Yaml::parse($resource->docker_compose_raw); @@ -647,7 +716,9 @@ function getTopLevelNetworks(Service|Application $resource) return $value == $networkName || $key == $networkName; }); if (! $networkExists) { - $topLevelNetworks->put($networkDetails, null); + if (is_string($networkDetails) || is_int($networkDetails)) { + $topLevelNetworks->put($networkDetails, null); + } } } } @@ -670,7 +741,7 @@ function getTopLevelNetworks(Service|Application $resource) return $topLevelNetworks->keys(); } - } elseif ($resource->getMorphClass() === 'App\Models\Application') { + } elseif ($resource->getMorphClass() === \App\Models\Application::class) { try { $yaml = Yaml::parse($resource->docker_compose_raw); } catch (\Exception $e) { @@ -697,7 +768,9 @@ function getTopLevelNetworks(Service|Application $resource) return $value == $networkName || $key == $networkName; }); if (! $networkExists) { - $topLevelNetworks->put($networkDetails, null); + if (is_string($networkDetails) || is_int($networkDetails)) { + $topLevelNetworks->put($networkDetails, null); + } } } } @@ -719,10 +792,710 @@ function getTopLevelNetworks(Service|Application $resource) return $topLevelNetworks->keys(); } } +function sourceIsLocal(Stringable $source) +{ + if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~') || $source->startsWith('..') || $source->startsWith('~/') || $source->startsWith('../')) { + return true; + } + + return false; +} + +function replaceLocalSource(Stringable $source, Stringable $replacedWith) +{ + if ($source->startsWith('.')) { + $source = $source->replaceFirst('.', $replacedWith->value()); + } + if ($source->startsWith('~')) { + $source = $source->replaceFirst('~', $replacedWith->value()); + } + if ($source->startsWith('..')) { + $source = $source->replaceFirst('..', $replacedWith->value()); + } + if ($source->endsWith('/') && $source->value() !== '/') { + $source = $source->replaceLast('/', ''); + } + + return $source; +} + +function convertToArray($collection) +{ + if ($collection instanceof Collection) { + return $collection->map(function ($item) { + return convertToArray($item); + })->toArray(); + } elseif ($collection instanceof Stringable) { + return (string) $collection; + } elseif (is_array($collection)) { + return array_map(function ($item) { + return convertToArray($item); + }, $collection); + } + + return $collection; +} + +function parseCommandFromMagicEnvVariable(Str|string $key): Stringable +{ + $value = str($key); + $count = substr_count($value->value(), '_'); + if ($count === 2) { + if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) { + // SERVICE_FQDN_UMAMI + $command = $value->after('SERVICE_')->beforeLast('_'); + } else { + // SERVICE_BASE64_UMAMI + $command = $value->after('SERVICE_')->beforeLast('_'); + } + } + if ($count === 3) { + if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) { + // SERVICE_FQDN_UMAMI_1000 + $command = $value->after('SERVICE_')->before('_'); + } else { + // SERVICE_BASE64_64_UMAMI + $command = $value->after('SERVICE_')->beforeLast('_'); + } + } + + return str($command); +} +function parseEnvVariable(Str|string $value) +{ + $value = str($value); + $count = substr_count($value->value(), '_'); + $command = null; + $forService = null; + $generatedValue = null; + $port = null; + if ($value->startsWith('SERVICE')) { + if ($count === 2) { + if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) { + // SERVICE_FQDN_UMAMI + $command = $value->after('SERVICE_')->beforeLast('_'); + $forService = $value->afterLast('_'); + } else { + // SERVICE_BASE64_UMAMI + $command = $value->after('SERVICE_')->beforeLast('_'); + } + } + if ($count === 3) { + if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) { + // SERVICE_FQDN_UMAMI_1000 + $command = $value->after('SERVICE_')->before('_'); + $forService = $value->after('SERVICE_')->after('_')->before('_'); + $port = $value->afterLast('_'); + if (filter_var($port, FILTER_VALIDATE_INT) === false) { + $port = null; + } + } else { + // SERVICE_BASE64_64_UMAMI + $command = $value->after('SERVICE_')->beforeLast('_'); + ray($command); + } + } + } + + return [ + 'command' => $command, + 'forService' => $forService, + 'generatedValue' => $generatedValue, + 'port' => $port, + ]; +} +function generateEnvValue(string $command, Service|Application|null $service = null) +{ + switch ($command) { + case 'PASSWORD': + $generatedValue = Str::password(symbols: false); + break; + case 'PASSWORD_64': + $generatedValue = Str::password(length: 64, symbols: false); + break; + // This is not base64, it's just a random string + case 'BASE64_64': + $generatedValue = Str::random(64); + break; + case 'BASE64_128': + $generatedValue = Str::random(128); + break; + case 'BASE64': + case 'BASE64_32': + $generatedValue = Str::random(32); + break; + // This is base64, + case 'REALBASE64_64': + $generatedValue = base64_encode(Str::random(64)); + break; + case 'REALBASE64_128': + $generatedValue = base64_encode(Str::random(128)); + break; + case 'REALBASE64': + case 'REALBASE64_32': + $generatedValue = base64_encode(Str::random(32)); + break; + case 'USER': + $generatedValue = Str::random(16); + break; + case 'SUPABASEANON': + $signingKey = $service->environment_variables()->where('key', 'SERVICE_PASSWORD_JWT')->first(); + if (is_null($signingKey)) { + return; + } else { + $signingKey = $signingKey->value; + } + $key = InMemory::plainText($signingKey); + $algorithm = new Sha256; + $tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default())); + $now = CarbonImmutable::now(); + $now = $now->setTime($now->format('H'), $now->format('i')); + $token = $tokenBuilder + ->issuedBy('supabase') + ->issuedAt($now) + ->expiresAt($now->modify('+100 year')) + ->withClaim('role', 'anon') + ->getToken($algorithm, $key); + $generatedValue = $token->toString(); + break; + case 'SUPABASESERVICE': + $signingKey = $service->environment_variables()->where('key', 'SERVICE_PASSWORD_JWT')->first(); + if (is_null($signingKey)) { + return; + } else { + $signingKey = $signingKey->value; + } + $key = InMemory::plainText($signingKey); + $algorithm = new Sha256; + $tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default())); + $now = CarbonImmutable::now(); + $now = $now->setTime($now->format('H'), $now->format('i')); + $token = $tokenBuilder + ->issuedBy('supabase') + ->issuedAt($now) + ->expiresAt($now->modify('+100 year')) + ->withClaim('role', 'service_role') + ->getToken($algorithm, $key); + $generatedValue = $token->toString(); + break; + default: + // $generatedValue = Str::random(16); + $generatedValue = null; + break; + } + + return $generatedValue; +} + +function getRealtime() +{ + $envDefined = env('PUSHER_PORT'); + if (empty($envDefined)) { + $url = Url::fromString(Request::getSchemeAndHttpHost()); + $port = $url->getPort(); + if ($port) { + return '6001'; + } else { + return null; + } + } else { + return $envDefined; + } +} + +function validate_dns_entry(string $fqdn, Server $server) +{ + // https://www.cloudflare.com/ips-v4/# + $cloudflare_ips = collect(['173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20', '197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/13', '172.64.0.0/13', '131.0.72.0/22']); + + $url = Url::fromString($fqdn); + $host = $url->getHost(); + if (str($host)->contains('sslip.io')) { + return true; + } + $settings = instanceSettings(); + $is_dns_validation_enabled = data_get($settings, 'is_dns_validation_enabled'); + if (! $is_dns_validation_enabled) { + return true; + } + $dns_servers = data_get($settings, 'custom_dns_servers'); + $dns_servers = str($dns_servers)->explode(','); + if ($server->id === 0) { + $ip = data_get($settings, 'public_ipv4', data_get($settings, 'public_ipv6', $server->ip)); + } else { + $ip = $server->ip; + } + $found_matching_ip = false; + $type = \PurplePixie\PhpDns\DNSTypes::NAME_A; + foreach ($dns_servers as $dns_server) { + try { + ray("Checking $host on $dns_server"); + $query = new DNSQuery($dns_server); + $results = $query->query($host, $type); + if ($results === false || $query->hasError()) { + ray('Error: '.$query->getLasterror()); + } else { + foreach ($results as $result) { + if ($result->getType() == $type) { + if (ip_match($result->getData(), $cloudflare_ips->toArray(), $match)) { + ray("Found match in Cloudflare IPs: $match"); + $found_matching_ip = true; + break; + } + if ($result->getData() === $ip) { + ray($host.' has IP address '.$result->getData()); + ray($result->getString()); + $found_matching_ip = true; + break; + } + } + } + } + } catch (\Exception) { + } + } + ray("Found match: $found_matching_ip"); + + return $found_matching_ip; +} + +function ip_match($ip, $cidrs, &$match = null) +{ + foreach ((array) $cidrs as $cidr) { + [$subnet, $mask] = explode('/', $cidr); + if (((ip2long($ip) & ($mask = ~((1 << (32 - $mask)) - 1))) == (ip2long($subnet) & $mask))) { + $match = $cidr; + + return true; + } + } + + return false; +} +function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId = null, ?string $uuid = null) +{ + if (is_null($teamId)) { + return response()->json(['error' => 'Team ID is required.'], 400); + } + if (is_array($domains)) { + $domains = collect($domains); + } + + $domains = $domains->map(function ($domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + + return str($domain); + }); + $applications = Application::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid']); + $serviceApplications = ServiceApplication::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid']); + if ($uuid) { + $applications = $applications->filter(fn ($app) => $app->uuid !== $uuid); + $serviceApplications = $serviceApplications->filter(fn ($app) => $app->uuid !== $uuid); + } + $domainFound = false; + foreach ($applications as $app) { + if (is_null($app->fqdn)) { + continue; + } + $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); + foreach ($list_of_domains as $domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + $domainFound = true; + break; + } + } + } + if ($domainFound) { + return true; + } + foreach ($serviceApplications as $app) { + if (str($app->fqdn)->isEmpty()) { + continue; + } + $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); + foreach ($list_of_domains as $domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + $domainFound = true; + break; + } + } + } + if ($domainFound) { + return true; + } + $settings = instanceSettings(); + if (data_get($settings, 'fqdn')) { + $domain = data_get($settings, 'fqdn'); + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + return true; + } + } +} +function check_domain_usage(ServiceApplication|Application|null $resource = null, ?string $domain = null) +{ + if ($resource) { + if ($resource->getMorphClass() === \App\Models\Application::class && $resource->build_pack === 'dockercompose') { + $domains = data_get(json_decode($resource->docker_compose_domains, true), '*.domain'); + $domains = collect($domains); + } else { + $domains = collect($resource->fqdns); + } + } elseif ($domain) { + $domains = collect($domain); + } else { + throw new \RuntimeException('No resource or FQDN provided.'); + } + $domains = $domains->map(function ($domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + + return str($domain); + }); + $apps = Application::all(); + foreach ($apps as $app) { + $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); + foreach ($list_of_domains as $domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + if (data_get($resource, 'uuid')) { + if ($resource->uuid !== $app->uuid) { + throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->name}"); + } + } elseif ($domain) { + throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->name}"); + } + } + } + } + $apps = ServiceApplication::all(); + foreach ($apps as $app) { + $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); + foreach ($list_of_domains as $domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + if (data_get($resource, 'uuid')) { + if ($resource->uuid !== $app->uuid) { + throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->service->name}"); + } + } elseif ($domain) { + throw new \RuntimeException("Domain $naked_domain is already in use by another resource:

Link: {$app->service->name}"); + } + } + } + } + if ($resource) { + $settings = instanceSettings(); + if (data_get($settings, 'fqdn')) { + $domain = data_get($settings, 'fqdn'); + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + throw new \RuntimeException("Domain $naked_domain is already in use by this Coolify instance."); + } + } + } +} + +function parseCommandsByLineForSudo(Collection $commands, Server $server): array +{ + $commands = $commands->map(function ($line) { + if ( + ! str(trim($line))->startsWith([ + 'cd', + 'command', + 'echo', + 'true', + 'if', + 'fi', + ]) + ) { + return "sudo $line"; + } + + if (str(trim($line))->startsWith('if')) { + return str_replace('if', 'if sudo', $line); + } + + return $line; + }); + + $commands = $commands->map(function ($line) use ($server) { + if (Str::startsWith($line, 'sudo mkdir -p')) { + return "$line && sudo chown -R $server->user:$server->user ".Str::after($line, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($line, 'sudo mkdir -p'); + } + + return $line; + }); + + $commands = $commands->map(function ($line) { + $line = str($line); + if (str($line)->contains('$(')) { + $line = $line->replace('$(', '$(sudo '); + } + if (str($line)->contains('||')) { + $line = $line->replace('||', '|| sudo'); + } + if (str($line)->contains('&&')) { + $line = $line->replace('&&', '&& sudo'); + } + if (str($line)->contains(' | ')) { + $line = $line->replace(' | ', ' | sudo '); + } + + return $line->value(); + }); + + return $commands->toArray(); +} +function parseLineForSudo(string $command, Server $server): string +{ + if (! str($command)->startSwith('cd') && ! str($command)->startSwith('command')) { + $command = "sudo $command"; + } + if (Str::startsWith($command, 'sudo mkdir -p')) { + $command = "$command && sudo chown -R $server->user:$server->user ".Str::after($command, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($command, 'sudo mkdir -p'); + } + if (str($command)->contains('$(') || str($command)->contains('`')) { + $command = str($command)->replace('$(', '$(sudo ')->replace('`', '`sudo ')->value(); + } + if (str($command)->contains('||')) { + $command = str($command)->replace('||', '|| sudo ')->value(); + } + if (str($command)->contains('&&')) { + $command = str($command)->replace('&&', '&& sudo ')->value(); + } + + return $command; +} + +function get_public_ips() +{ + try { + [$first, $second] = Process::concurrently(function (Pool $pool) { + $pool->path(__DIR__)->command('curl -4s https://ifconfig.io'); + $pool->path(__DIR__)->command('curl -6s https://ifconfig.io'); + }); + $ipv4 = $first->output(); + if ($ipv4) { + $ipv4 = trim($ipv4); + $validate_ipv4 = filter_var($ipv4, FILTER_VALIDATE_IP); + if ($validate_ipv4 == false) { + echo "Invalid ipv4: $ipv4\n"; + + return; + } + InstanceSettings::get()->update(['public_ipv4' => $ipv4]); + } + } catch (\Exception $e) { + echo "Error: {$e->getMessage()}\n"; + } + try { + $ipv6 = $second->output(); + if ($ipv6) { + $ipv6 = trim($ipv6); + $validate_ipv6 = filter_var($ipv6, FILTER_VALIDATE_IP); + if ($validate_ipv6 == false) { + echo "Invalid ipv6: $ipv6\n"; + + return; + } + InstanceSettings::get()->update(['public_ipv6' => $ipv6]); + } + } catch (\Throwable $e) { + echo "Error: {$e->getMessage()}\n"; + } +} + +function isAnyDeploymentInprogress() +{ + // Only use it in the deployment script + $count = ApplicationDeploymentQueue::whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS, ApplicationDeploymentStatus::QUEUED])->count(); + if ($count > 0) { + echo "There are $count deployments in progress. Exiting...\n"; + exit(1); + } + echo "No deployments in progress.\n"; + exit(0); +} + +function isBase64Encoded($strValue) +{ + return base64_encode(base64_decode($strValue, true)) === $strValue; +} +function customApiValidator(Collection|array $item, array $rules) +{ + if (is_array($item)) { + $item = collect($item); + } + + return Validator::make($item->toArray(), $rules, [ + 'required' => 'This field is required.', + ]); +} + +function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull_request_id = 0) +{ + $serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $pull_request_id) { + $type = null; + $source = null; + $target = null; + $content = null; + $isDirectory = false; + if (is_string($volume)) { + $source = str($volume)->before(':'); + $target = str($volume)->after(':')->beforeLast(':'); + $foundConfig = $resource->fileStorages()->whereMountPath($target)->first(); + if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~')) { + $type = str('bind'); + if ($foundConfig) { + $contentNotNull = data_get($foundConfig, 'content'); + if ($contentNotNull) { + $content = $contentNotNull; + } + $isDirectory = data_get($foundConfig, 'is_directory'); + } else { + // By default, we cannot determine if the bind is a directory or not, so we set it to directory + $isDirectory = true; + } + } else { + $type = str('volume'); + } + } elseif (is_array($volume)) { + $type = data_get_str($volume, 'type'); + $source = data_get_str($volume, 'source'); + $target = data_get_str($volume, 'target'); + $content = data_get($volume, 'content'); + $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null); + $foundConfig = $resource->fileStorages()->whereMountPath($target)->first(); + if ($foundConfig) { + $contentNotNull = data_get($foundConfig, 'content'); + if ($contentNotNull) { + $content = $contentNotNull; + } + $isDirectory = data_get($foundConfig, 'is_directory'); + } else { + $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null); + if ((is_null($isDirectory) || ! $isDirectory) && is_null($content)) { + // if isDirectory is not set (or false) & content is also not set, we assume it is a directory + ray('setting isDirectory to true'); + $isDirectory = true; + } + } + } + if ($type?->value() === 'bind') { + if ($source->value() === '/var/run/docker.sock') { + return $volume; + } + if ($source->value() === '/tmp' || $source->value() === '/tmp/') { + return $volume; + } + if (get_class($resource) === \App\Models\Application::class) { + $dir = base_configuration_dir().'/applications/'.$resource->uuid; + } else { + $dir = base_configuration_dir().'/services/'.$resource->service->uuid; + } + + if ($source->startsWith('.')) { + $source = $source->replaceFirst('.', $dir); + } + if ($source->startsWith('~')) { + $source = $source->replaceFirst('~', $dir); + } + if ($pull_request_id !== 0) { + $source = $source."-pr-$pull_request_id"; + } + if (! $resource?->settings?->is_preserve_repository_enabled || $foundConfig?->is_based_on_git) { + LocalFileVolume::updateOrCreate( + [ + 'mount_path' => $target, + 'resource_id' => $resource->id, + 'resource_type' => get_class($resource), + ], + [ + 'fs_path' => $source, + 'mount_path' => $target, + 'content' => $content, + 'is_directory' => $isDirectory, + 'resource_id' => $resource->id, + 'resource_type' => get_class($resource), + ] + ); + } + } elseif ($type->value() === 'volume') { + if ($topLevelVolumes->has($source->value())) { + $v = $topLevelVolumes->get($source->value()); + if (data_get($v, 'driver_opts.type') === 'cifs') { + return $volume; + } + } + $slugWithoutUuid = Str::slug($source, '-'); + if (get_class($resource) === \App\Models\Application::class) { + $name = "{$resource->uuid}_{$slugWithoutUuid}"; + } else { + $name = "{$resource->service->uuid}_{$slugWithoutUuid}"; + } + if (is_string($volume)) { + $source = str($volume)->before(':'); + $target = str($volume)->after(':')->beforeLast(':'); + $source = $name; + $volume = "$source:$target"; + } elseif (is_array($volume)) { + data_set($volume, 'source', $name); + } + $topLevelVolumes->put($name, [ + 'name' => $name, + ]); + LocalPersistentVolume::updateOrCreate( + [ + 'mount_path' => $target, + 'resource_id' => $resource->id, + 'resource_type' => get_class($resource), + ], + [ + 'name' => $name, + 'mount_path' => $target, + 'resource_id' => $resource->id, + 'resource_type' => get_class($resource), + ] + ); + } + dispatch(new ServerFilesFromServerJob($resource)); + + return $volume; + }); + + return [ + 'serviceVolumes' => $serviceVolumes, + 'topLevelVolumes' => $topLevelVolumes, + ]; +} function parseDockerComposeFile(Service|Application $resource, bool $isNew = false, int $pull_request_id = 0, ?int $preview_id = null) { - if ($resource->getMorphClass() === 'App\Models\Service') { + if ($resource->getMorphClass() === \App\Models\Service::class) { if ($resource->docker_compose_raw) { try { $yaml = Yaml::parse($resource->docker_compose_raw); @@ -732,6 +1505,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $allServices = get_service_templates(); $topLevelVolumes = collect(data_get($yaml, 'volumes', [])); $topLevelNetworks = collect(data_get($yaml, 'networks', [])); + $topLevelConfigs = collect(data_get($yaml, 'configs', [])); + $topLevelSecrets = collect(data_get($yaml, 'secrets', [])); $services = data_get($yaml, 'services'); $generatedServiceFQDNS = collect([]); @@ -864,7 +1639,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal return $value == $networkName || $key == $networkName; }); if (! $networkExists) { - $topLevelNetworks->put($networkDetails, null); + if (is_string($networkDetails) || is_int($networkDetails)) { + $topLevelNetworks->put($networkDetails, null); + } } } } @@ -929,26 +1706,33 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $content = null; $isDirectory = false; if (is_string($volume)) { - $source = Str::of($volume)->before(':'); - $target = Str::of($volume)->after(':')->beforeLast(':'); + $source = str($volume)->before(':'); + $target = str($volume)->after(':')->beforeLast(':'); if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~')) { - $type = Str::of('bind'); + $type = str('bind'); + // By default, we cannot determine if the bind is a directory or not, so we set it to directory + $isDirectory = true; } else { - $type = Str::of('volume'); + $type = str('volume'); } } elseif (is_array($volume)) { $type = data_get_str($volume, 'type'); $source = data_get_str($volume, 'source'); $target = data_get_str($volume, 'target'); $content = data_get($volume, 'content'); - $isDirectory = (bool) data_get($volume, 'isDirectory', false) || (bool) data_get($volume, 'is_directory', false); + $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null); $foundConfig = $savedService->fileStorages()->whereMountPath($target)->first(); if ($foundConfig) { $contentNotNull = data_get($foundConfig, 'content'); if ($contentNotNull) { $content = $contentNotNull; } - $isDirectory = (bool) data_get($volume, 'isDirectory', false) || (bool) data_get($volume, 'is_directory', false); + $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null); + } + if (is_null($isDirectory) && is_null($content)) { + // if isDirectory is not set & content is also not set, we assume it is a directory + ray('setting isDirectory to true'); + $isDirectory = true; } } if ($type?->value() === 'bind') { @@ -983,8 +1767,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $slugWithoutUuid = Str::slug($source, '-'); $name = "{$savedService->service->uuid}_{$slugWithoutUuid}"; if (is_string($volume)) { - $source = Str::of($volume)->before(':'); - $target = Str::of($volume)->after(':')->beforeLast(':'); + $source = str($volume)->before(':'); + $target = str($volume)->after(':')->beforeLast(':'); $source = $name; $volume = "$source:$target"; } elseif (is_array($volume)) { @@ -1014,36 +1798,49 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal data_set($service, 'volumes', $serviceVolumes->toArray()); } - // Add env_file with at least .env to the service - // $envFile = collect(data_get($service, 'env_file', [])); - // if ($envFile->count() > 0) { - // if (!$envFile->contains('.env')) { - // $envFile->push('.env'); - // } - // } else { - // $envFile = collect(['.env']); - // } - // data_set($service, 'env_file', $envFile->toArray()); - + // convert - SESSION_SECRET: 123 to - SESSION_SECRET=123 + $convertedServiceVariables = collect([]); + foreach ($serviceVariables as $variableName => $variable) { + if (is_numeric($variableName)) { + if (is_array($variable)) { + $key = str(collect($variable)->keys()->first()); + $value = str(collect($variable)->values()->first()); + $variable = "$key=$value"; + $convertedServiceVariables->put($variableName, $variable); + } elseif (is_string($variable)) { + $convertedServiceVariables->put($variableName, $variable); + } + } elseif (is_string($variableName)) { + $convertedServiceVariables->put($variableName, $variable); + } + } + $serviceVariables = $convertedServiceVariables; // Get variables from the service foreach ($serviceVariables as $variableName => $variable) { if (is_numeric($variableName)) { - $variable = Str::of($variable); - if ($variable->contains('=')) { - // - SESSION_SECRET=123 - // - SESSION_SECRET= - $key = $variable->before('='); - $value = $variable->after('='); + if (is_array($variable)) { + // - SESSION_SECRET: 123 + // - SESSION_SECRET: + $key = str(collect($variable)->keys()->first()); + $value = str(collect($variable)->values()->first()); } else { - // - SESSION_SECRET - $key = $variable; - $value = null; + $variable = str($variable); + if ($variable->contains('=')) { + // - SESSION_SECRET=123 + // - SESSION_SECRET= + $key = $variable->before('='); + $value = $variable->after('='); + } else { + // - SESSION_SECRET + $key = $variable; + $value = null; + } } } else { // SESSION_SECRET: 123 // SESSION_SECRET: - $key = Str::of($variableName); - $value = Str::of($variable); + $key = str($variableName); + $value = str($variable); } if ($key->startsWith('SERVICE_FQDN')) { if ($isNew || $savedService->fqdn === null) { @@ -1133,7 +1930,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'key' => $key, 'service_id' => $resource->id, ])->first(); - $value = Str::of(replaceVariables($value)); + $value = replaceVariables($value); $key = $value; if ($value->startsWith('SERVICE_')) { $foundEnv = EnvironmentVariable::where([ @@ -1166,7 +1963,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal // } } else { if ($command->value() === 'URL') { - $fqdn = Str::of($fqdn)->after('://')->value(); + $fqdn = str($fqdn)->after('://')->value(); } EnvironmentVariable::create([ 'key' => $key, @@ -1261,38 +2058,62 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $serviceLabels = $serviceLabels->merge($defaultLabels); if (! $isDatabase && $fqdns->count() > 0) { if ($fqdns) { - $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( - uuid: $resource->uuid, - domains: $fqdns, - is_force_https_enabled: true, - serviceLabels: $serviceLabels, - is_gzip_enabled: $savedService->isGzipEnabled(), - is_stripprefix_enabled: $savedService->isStripprefixEnabled(), - service_name: $serviceName, - image: data_get($service, 'image') - )); - $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( - network: $resource->destination->network, - uuid: $resource->uuid, - domains: $fqdns, - is_force_https_enabled: true, - serviceLabels: $serviceLabels, - is_gzip_enabled: $savedService->isGzipEnabled(), - is_stripprefix_enabled: $savedService->isStripprefixEnabled(), - service_name: $serviceName, - image: data_get($service, 'image') - )); + $shouldGenerateLabelsExactly = $resource->server->settings->generate_exact_labels; + if ($shouldGenerateLabelsExactly) { + switch ($resource->server->proxyType()) { + case ProxyTypes::TRAEFIK->value: + $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( + uuid: $resource->uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $savedService->isGzipEnabled(), + is_stripprefix_enabled: $savedService->isStripprefixEnabled(), + service_name: $serviceName, + image: data_get($service, 'image') + )); + break; + case ProxyTypes::CADDY->value: + $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( + network: $resource->destination->network, + uuid: $resource->uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $savedService->isGzipEnabled(), + is_stripprefix_enabled: $savedService->isStripprefixEnabled(), + service_name: $serviceName, + image: data_get($service, 'image') + )); + break; + } + } else { + $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( + uuid: $resource->uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $savedService->isGzipEnabled(), + is_stripprefix_enabled: $savedService->isStripprefixEnabled(), + service_name: $serviceName, + image: data_get($service, 'image') + )); + $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( + network: $resource->destination->network, + uuid: $resource->uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $savedService->isGzipEnabled(), + is_stripprefix_enabled: $savedService->isStripprefixEnabled(), + service_name: $serviceName, + image: data_get($service, 'image') + )); + } } } if ($resource->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) { - data_set($service, 'logging', [ - 'driver' => 'fluentd', - 'options' => [ - 'fluentd-address' => 'tcp://127.0.0.1:24224', - 'fluentd-async' => 'true', - 'fluentd-sub-second-precision' => 'true', - ], - ]); + data_set($service, 'logging', generate_fluentd_configuration()); } if ($serviceLabels->count() > 0) { if ($resource->is_container_label_escape_enabled) { @@ -1314,31 +2135,73 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal data_forget($service, 'volumes.*.isDirectory'); data_forget($service, 'volumes.*.is_directory'); data_forget($service, 'exclude_from_hc'); - - // Remove unnecessary variables from service.environment - // $withoutServiceEnvs = collect([]); - // collect(data_get($service, 'environment'))->each(function ($value, $key) use ($withoutServiceEnvs) { - // ray($key, $value); - // if (!Str::of($key)->startsWith('$SERVICE_') && !Str::of($value)->startsWith('SERVICE_')) { - // $k = Str::of($value)->before("="); - // $v = Str::of($value)->after("="); - // $withoutServiceEnvs->put($k->value(), $v->value()); - // } - // }); - // ray($withoutServiceEnvs); - // data_set($service, 'environment', $withoutServiceEnvs->toArray()); + data_set($service, 'environment', $serviceVariables->toArray()); updateCompose($savedService); return $service; }); + + $envs_from_coolify = $resource->environment_variables()->get(); + $services = collect($services)->map(function ($service, $serviceName) use ($resource, $envs_from_coolify) { + $serviceVariables = collect(data_get($service, 'environment', [])); + $parsedServiceVariables = collect([]); + foreach ($serviceVariables as $key => $value) { + if (is_numeric($key)) { + $value = str($value); + if ($value->contains('=')) { + $key = $value->before('=')->value(); + $value = $value->after('=')->value(); + } else { + $key = $value->value(); + $value = null; + } + $parsedServiceVariables->put($key, $value); + } else { + $parsedServiceVariables->put($key, $value); + } + } + $parsedServiceVariables->put('COOLIFY_CONTAINER_NAME', "$serviceName-{$resource->uuid}"); + + // TODO: move this in a shared function + if (! $parsedServiceVariables->has('COOLIFY_APP_NAME')) { + $parsedServiceVariables->put('COOLIFY_APP_NAME', "\"{$resource->name}\""); + } + if (! $parsedServiceVariables->has('COOLIFY_SERVER_IP')) { + $parsedServiceVariables->put('COOLIFY_SERVER_IP', "\"{$resource->destination->server->ip}\""); + } + if (! $parsedServiceVariables->has('COOLIFY_ENVIRONMENT_NAME')) { + $parsedServiceVariables->put('COOLIFY_ENVIRONMENT_NAME', "\"{$resource->environment->name}\""); + } + if (! $parsedServiceVariables->has('COOLIFY_PROJECT_NAME')) { + $parsedServiceVariables->put('COOLIFY_PROJECT_NAME', "\"{$resource->project()->name}\""); + } + + $parsedServiceVariables = $parsedServiceVariables->map(function ($value, $key) use ($envs_from_coolify) { + if (! str($value)->startsWith('$')) { + $found_env = $envs_from_coolify->where('key', $key)->first(); + if ($found_env) { + return $found_env->value; + } + } + + return $value; + }); + + data_set($service, 'environment', $parsedServiceVariables->toArray()); + + return $service; + }); $finalServices = [ 'services' => $services->toArray(), 'volumes' => $topLevelVolumes->toArray(), 'networks' => $topLevelNetworks->toArray(), + 'configs' => $topLevelConfigs->toArray(), + 'secrets' => $topLevelSecrets->toArray(), ]; $yaml = data_forget($yaml, 'services.*.volumes.*.content'); $resource->docker_compose_raw = Yaml::dump($yaml, 10, 2); $resource->docker_compose = Yaml::dump($finalServices, 10, 2); + $resource->save(); $resource->saveComposeConfigs(); @@ -1346,14 +2209,10 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } else { return collect([]); } - } elseif ($resource->getMorphClass() === 'App\Models\Application') { - $isSameDockerComposeFile = false; - if ($resource->dockerComposePrLocation() === $resource->dockerComposeLocation()) { - $isSameDockerComposeFile = true; - } + } elseif ($resource->getMorphClass() === \App\Models\Application::class) { try { $yaml = Yaml::parse($resource->docker_compose_raw); - } catch (\Exception $e) { + } catch (\Exception) { return; } $server = $resource->destination->server; @@ -1374,6 +2233,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } $topLevelNetworks = collect(data_get($yaml, 'networks', [])); + $topLevelConfigs = collect(data_get($yaml, 'configs', [])); + $topLevelSecrets = collect(data_get($yaml, 'secrets', [])); $services = data_get($yaml, 'services'); $generatedServiceFQDNS = collect([]); @@ -1415,128 +2276,259 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $baseName = generateApplicationContainerName($resource, $pull_request_id); $containerName = "$serviceName-$baseName"; - if (count($serviceVolumes) > 0) { - $serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $pull_request_id) { - if (is_string($volume)) { - $volume = str($volume); - if ($volume->contains(':') && ! $volume->startsWith('/')) { - $name = $volume->before(':'); - $mount = $volume->after(':'); - if ($name->startsWith('.') || $name->startsWith('~')) { - $dir = base_configuration_dir().'/applications/'.$resource->uuid; - if ($name->startsWith('.')) { - $name = $name->replaceFirst('.', $dir); - } - if ($name->startsWith('~')) { - $name = $name->replaceFirst('~', $dir); - } - if ($pull_request_id !== 0) { - $name = $name."-pr-$pull_request_id"; - } - $volume = str("$name:$mount"); - } else { - if ($pull_request_id !== 0) { - $name = $name."-pr-$pull_request_id"; - $volume = str("$name:$mount"); - if ($topLevelVolumes->has($name)) { - $v = $topLevelVolumes->get($name); - if (data_get($v, 'driver_opts.type') === 'cifs') { - // Do nothing - } else { - if (is_null(data_get($v, 'name'))) { - data_set($v, 'name', $name); - data_set($topLevelVolumes, $name, $v); - } - } - } else { - $topLevelVolumes->put($name, [ - 'name' => $name, - ]); - } - } else { - if ($topLevelVolumes->has($name->value())) { - $v = $topLevelVolumes->get($name->value()); - if (data_get($v, 'driver_opts.type') === 'cifs') { - // Do nothing - } else { - if (is_null(data_get($v, 'name'))) { - data_set($topLevelVolumes, $name->value(), $v); - } - } - } else { - $topLevelVolumes->put($name->value(), [ - 'name' => $name->value(), - ]); - } - } - } - } else { - if ($volume->startsWith('/')) { + if ($resource->compose_parsing_version === '1') { + if (count($serviceVolumes) > 0) { + $serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $pull_request_id) { + if (is_string($volume)) { + $volume = str($volume); + if ($volume->contains(':') && ! $volume->startsWith('/')) { $name = $volume->before(':'); $mount = $volume->after(':'); - if ($pull_request_id !== 0) { - $name = $name."-pr-$pull_request_id"; - } - $volume = str("$name:$mount"); - } - } - } elseif (is_array($volume)) { - $source = data_get($volume, 'source'); - $target = data_get($volume, 'target'); - $read_only = data_get($volume, 'read_only'); - if ($source && $target) { - if ((str($source)->startsWith('.') || str($source)->startsWith('~'))) { - $dir = base_configuration_dir().'/applications/'.$resource->uuid; - if (str($source, '.')) { - $source = str($source)->replaceFirst('.', $dir); - } - if (str($source, '~')) { - $source = str($source)->replaceFirst('~', $dir); - } - if ($pull_request_id !== 0) { - $source = $source."-pr-$pull_request_id"; - } - if ($read_only) { - data_set($volume, 'source', $source.':'.$target.':ro'); + if ($name->startsWith('.') || $name->startsWith('~')) { + $dir = base_configuration_dir().'/applications/'.$resource->uuid; + if ($name->startsWith('.')) { + $name = $name->replaceFirst('.', $dir); + } + if ($name->startsWith('~')) { + $name = $name->replaceFirst('~', $dir); + } + if ($pull_request_id !== 0) { + $name = $name."-pr-$pull_request_id"; + } + $volume = str("$name:$mount"); } else { - data_set($volume, 'source', $source.':'.$target); - } - } else { - if ($pull_request_id !== 0) { - $source = $source."-pr-$pull_request_id"; - } - if ($read_only) { - data_set($volume, 'source', $source.':'.$target.':ro'); - } else { - data_set($volume, 'source', $source.':'.$target); - } - if (! str($source)->startsWith('/')) { - if ($topLevelVolumes->has($source)) { - $v = $topLevelVolumes->get($source); - if (data_get($v, 'driver_opts.type') === 'cifs') { - // Do nothing - } else { - if (is_null(data_get($v, 'name'))) { - data_set($v, 'name', $source); - data_set($topLevelVolumes, $source, $v); + if ($pull_request_id !== 0) { + $name = $name."-pr-$pull_request_id"; + $volume = str("$name:$mount"); + if ($topLevelVolumes->has($name)) { + $v = $topLevelVolumes->get($name); + if (data_get($v, 'driver_opts.type') === 'cifs') { + // Do nothing + } else { + if (is_null(data_get($v, 'name'))) { + data_set($v, 'name', $name); + data_set($topLevelVolumes, $name, $v); + } } + } else { + $topLevelVolumes->put($name, [ + 'name' => $name, + ]); } } else { - $topLevelVolumes->put($source, [ - 'name' => $source, - ]); + if ($topLevelVolumes->has($name->value())) { + $v = $topLevelVolumes->get($name->value()); + if (data_get($v, 'driver_opts.type') === 'cifs') { + // Do nothing + } else { + if (is_null(data_get($v, 'name'))) { + data_set($topLevelVolumes, $name->value(), $v); + } + } + } else { + $topLevelVolumes->put($name->value(), [ + 'name' => $name->value(), + ]); + } + } + } + } else { + if ($volume->startsWith('/')) { + $name = $volume->before(':'); + $mount = $volume->after(':'); + if ($pull_request_id !== 0) { + $name = $name."-pr-$pull_request_id"; + } + $volume = str("$name:$mount"); + } + } + } elseif (is_array($volume)) { + $source = data_get($volume, 'source'); + $target = data_get($volume, 'target'); + $read_only = data_get($volume, 'read_only'); + if ($source && $target) { + if ((str($source)->startsWith('.') || str($source)->startsWith('~'))) { + $dir = base_configuration_dir().'/applications/'.$resource->uuid; + if (str($source, '.')) { + $source = str($source)->replaceFirst('.', $dir); + } + if (str($source, '~')) { + $source = str($source)->replaceFirst('~', $dir); + } + if ($pull_request_id !== 0) { + $source = $source."-pr-$pull_request_id"; + } + if ($read_only) { + data_set($volume, 'source', $source.':'.$target.':ro'); + } else { + data_set($volume, 'source', $source.':'.$target); + } + } else { + if ($pull_request_id !== 0) { + $source = $source."-pr-$pull_request_id"; + } + if ($read_only) { + data_set($volume, 'source', $source.':'.$target.':ro'); + } else { + data_set($volume, 'source', $source.':'.$target); + } + if (! str($source)->startsWith('/')) { + if ($topLevelVolumes->has($source)) { + $v = $topLevelVolumes->get($source); + if (data_get($v, 'driver_opts.type') === 'cifs') { + // Do nothing + } else { + if (is_null(data_get($v, 'name'))) { + data_set($v, 'name', $source); + data_set($topLevelVolumes, $source, $v); + } + } + } else { + $topLevelVolumes->put($source, [ + 'name' => $source, + ]); + } } } } } - } - if (is_array($volume)) { - return data_get($volume, 'source'); - } + if (is_array($volume)) { + return data_get($volume, 'source'); + } - return $volume->value(); - }); - data_set($service, 'volumes', $serviceVolumes->toArray()); + return $volume->value(); + }); + data_set($service, 'volumes', $serviceVolumes->toArray()); + } + } elseif ($resource->compose_parsing_version === '2') { + if (count($serviceVolumes) > 0) { + $serviceVolumes = $serviceVolumes->map(function ($volume) use ($resource, $topLevelVolumes, $pull_request_id) { + if (is_string($volume)) { + $volume = str($volume); + if ($volume->contains(':') && ! $volume->startsWith('/')) { + $name = $volume->before(':'); + $mount = $volume->after(':'); + if ($name->startsWith('.') || $name->startsWith('~')) { + $dir = base_configuration_dir().'/applications/'.$resource->uuid; + if ($name->startsWith('.')) { + $name = $name->replaceFirst('.', $dir); + } + if ($name->startsWith('~')) { + $name = $name->replaceFirst('~', $dir); + } + if ($pull_request_id !== 0) { + $name = $name."-pr-$pull_request_id"; + } + $volume = str("$name:$mount"); + } else { + if ($pull_request_id !== 0) { + $uuid = $resource->uuid; + $name = $uuid."-$name-pr-$pull_request_id"; + $volume = str("$name:$mount"); + if ($topLevelVolumes->has($name)) { + $v = $topLevelVolumes->get($name); + if (data_get($v, 'driver_opts.type') === 'cifs') { + // Do nothing + } else { + if (is_null(data_get($v, 'name'))) { + data_set($v, 'name', $name); + data_set($topLevelVolumes, $name, $v); + } + } + } else { + $topLevelVolumes->put($name, [ + 'name' => $name, + ]); + } + } else { + $uuid = $resource->uuid; + $name = str($uuid."-$name"); + $volume = str("$name:$mount"); + if ($topLevelVolumes->has($name->value())) { + $v = $topLevelVolumes->get($name->value()); + if (data_get($v, 'driver_opts.type') === 'cifs') { + // Do nothing + } else { + if (is_null(data_get($v, 'name'))) { + data_set($topLevelVolumes, $name->value(), $v); + } + } + } else { + $topLevelVolumes->put($name->value(), [ + 'name' => $name->value(), + ]); + } + } + } + } else { + if ($volume->startsWith('/')) { + $name = $volume->before(':'); + $mount = $volume->after(':'); + if ($pull_request_id !== 0) { + $name = $name."-pr-$pull_request_id"; + } + $volume = str("$name:$mount"); + } + } + } elseif (is_array($volume)) { + $source = data_get($volume, 'source'); + $target = data_get($volume, 'target'); + $read_only = data_get($volume, 'read_only'); + if ($source && $target) { + $uuid = $resource->uuid; + if ((str($source)->startsWith('.') || str($source)->startsWith('~') || str($source)->startsWith('/'))) { + $dir = base_configuration_dir().'/applications/'.$resource->uuid; + if (str($source, '.')) { + $source = str($source)->replaceFirst('.', $dir); + } + if (str($source, '~')) { + $source = str($source)->replaceFirst('~', $dir); + } + if ($read_only) { + data_set($volume, 'source', $source.':'.$target.':ro'); + } else { + data_set($volume, 'source', $source.':'.$target); + } + } else { + if ($pull_request_id === 0) { + $source = $uuid."-$source"; + } else { + $source = $uuid."-$source-pr-$pull_request_id"; + } + if ($read_only) { + data_set($volume, 'source', $source.':'.$target.':ro'); + } else { + data_set($volume, 'source', $source.':'.$target); + } + if (! str($source)->startsWith('/')) { + if ($topLevelVolumes->has($source)) { + $v = $topLevelVolumes->get($source); + if (data_get($v, 'driver_opts.type') === 'cifs') { + // Do nothing + } else { + if (is_null(data_get($v, 'name'))) { + data_set($v, 'name', $source); + data_set($topLevelVolumes, $source, $v); + } + } + } else { + $topLevelVolumes->put($source, [ + 'name' => $source, + ]); + } + } + } + } + } + if (is_array($volume)) { + return data_get($volume, 'source'); + } + dispatch(new ServerFilesFromServerJob($resource)); + + return $volume->value(); + }); + data_set($service, 'volumes', $serviceVolumes->toArray()); + } } if ($pull_request_id !== 0 && count($serviceDependencies) > 0) { @@ -1564,7 +2556,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal return $value == $networkName || $key == $networkName; }); if (! $networkExists) { - $topLevelNetworks->put($networkDetails, null); + if (is_string($networkDetails) || is_int($networkDetails)) { + $topLevelNetworks->put($networkDetails, null); + } } } } @@ -1584,7 +2578,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } } if ($collectedPorts->count() > 0) { - // ray($collectedPorts->implode(',')); + ray($collectedPorts->implode(',')); } $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) { return $value == $definedNetwork; @@ -1633,22 +2627,29 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal // Get variables from the service foreach ($serviceVariables as $variableName => $variable) { if (is_numeric($variableName)) { - $variable = Str::of($variable); - if ($variable->contains('=')) { - // - SESSION_SECRET=123 - // - SESSION_SECRET= - $key = $variable->before('='); - $value = $variable->after('='); + if (is_array($variable)) { + // - SESSION_SECRET: 123 + // - SESSION_SECRET: + $key = str(collect($variable)->keys()->first()); + $value = str(collect($variable)->values()->first()); } else { - // - SESSION_SECRET - $key = $variable; - $value = null; + $variable = str($variable); + if ($variable->contains('=')) { + // - SESSION_SECRET=123 + // - SESSION_SECRET= + $key = $variable->before('='); + $value = $variable->after('='); + } else { + // - SESSION_SECRET + $key = $variable; + $value = null; + } } } else { // SESSION_SECRET: 123 // SESSION_SECRET: - $key = Str::of($variableName); - $value = Str::of($variable); + $key = str($variableName); + $value = str($variable); } if ($key->startsWith('SERVICE_FQDN')) { if ($isNew) { @@ -1692,7 +2693,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'application_id' => $resource->id, 'is_preview' => false, ])->first(); - $value = Str::of(replaceVariables($value)); + $value = replaceVariables($value); $key = $value; if ($value->startsWith('SERVICE_')) { $foundEnv = EnvironmentVariable::where([ @@ -1714,7 +2715,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $fqdn = data_get($foundEnv, 'value'); } else { if ($command?->value() === 'URL') { - $fqdn = Str::of($fqdn)->after('://')->value(); + $fqdn = str($fqdn)->after('://')->value(); } EnvironmentVariable::create([ 'key' => $key, @@ -1808,7 +2809,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $template = $resource->preview_url_template; $host = $url->getHost(); $schema = $url->getScheme(); - $random = new Cuid2(7); + $random = new Cuid2; $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $pull_request_id, $preview_fqdn); @@ -1820,35 +2821,74 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal }); } } - $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( - uuid: $resource->uuid, - domains: $fqdns, - serviceLabels: $serviceLabels, - generate_unique_uuid: $resource->build_pack === 'dockercompose', - image: data_get($service, 'image') - )); - $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( - network: $resource->destination->network, - uuid: $resource->uuid, - domains: $fqdns, - serviceLabels: $serviceLabels, - image: data_get($service, 'image') - )); + $shouldGenerateLabelsExactly = $server->settings->generate_exact_labels; + if ($shouldGenerateLabelsExactly) { + switch ($server->proxyType()) { + case ProxyTypes::TRAEFIK->value: + $serviceLabels = $serviceLabels->merge( + fqdnLabelsForTraefik( + uuid: $resource->uuid, + domains: $fqdns, + serviceLabels: $serviceLabels, + generate_unique_uuid: $resource->build_pack === 'dockercompose', + image: data_get($service, 'image'), + is_force_https_enabled: $resource->isForceHttpsEnabled(), + is_gzip_enabled: $resource->isGzipEnabled(), + is_stripprefix_enabled: $resource->isStripprefixEnabled(), + ) + ); + break; + case ProxyTypes::CADDY->value: + $serviceLabels = $serviceLabels->merge( + fqdnLabelsForCaddy( + network: $resource->destination->network, + uuid: $resource->uuid, + domains: $fqdns, + serviceLabels: $serviceLabels, + image: data_get($service, 'image'), + is_force_https_enabled: $resource->isForceHttpsEnabled(), + is_gzip_enabled: $resource->isGzipEnabled(), + is_stripprefix_enabled: $resource->isStripprefixEnabled(), + ) + ); + break; + } + } else { + $serviceLabels = $serviceLabels->merge( + fqdnLabelsForTraefik( + uuid: $resource->uuid, + domains: $fqdns, + serviceLabels: $serviceLabels, + generate_unique_uuid: $resource->build_pack === 'dockercompose', + image: data_get($service, 'image'), + is_force_https_enabled: $resource->isForceHttpsEnabled(), + is_gzip_enabled: $resource->isGzipEnabled(), + is_stripprefix_enabled: $resource->isStripprefixEnabled(), + ) + ); + $serviceLabels = $serviceLabels->merge( + fqdnLabelsForCaddy( + network: $resource->destination->network, + uuid: $resource->uuid, + domains: $fqdns, + serviceLabels: $serviceLabels, + image: data_get($service, 'image'), + is_force_https_enabled: $resource->isForceHttpsEnabled(), + is_gzip_enabled: $resource->isGzipEnabled(), + is_stripprefix_enabled: $resource->isStripprefixEnabled(), + ) + ); + } } } } $defaultLabels = defaultLabels($resource->id, $containerName, $pull_request_id, type: 'application'); $serviceLabels = $serviceLabels->merge($defaultLabels); - if ($server->isLogDrainEnabled() && $resource->isLogDrainEnabled()) { - data_set($service, 'logging', [ - 'driver' => 'fluentd', - 'options' => [ - 'fluentd-address' => 'tcp://127.0.0.1:24224', - 'fluentd-async' => 'true', - 'fluentd-sub-second-precision' => 'true', - ], - ]); + if ($server->isLogDrainEnabled()) { + if ($resource instanceof Application && $resource->isLogDrainEnabled()) { + data_set($service, 'logging', generate_fluentd_configuration()); + } } if ($serviceLabels->count() > 0) { if ($resource->settings->is_container_label_escape_enabled) { @@ -1878,407 +2918,1228 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'services' => $services->toArray(), 'volumes' => $topLevelVolumes->toArray(), 'networks' => $topLevelNetworks->toArray(), + 'configs' => $topLevelConfigs->toArray(), + 'secrets' => $topLevelSecrets->toArray(), ]; - if ($isSameDockerComposeFile) { - $resource->docker_compose_pr_raw = Yaml::dump($yaml, 10, 2); - $resource->docker_compose_pr = Yaml::dump($finalServices, 10, 2); - $resource->docker_compose_raw = Yaml::dump($yaml, 10, 2); - $resource->docker_compose = Yaml::dump($finalServices, 10, 2); - } else { - $resource->docker_compose_raw = Yaml::dump($yaml, 10, 2); - $resource->docker_compose = Yaml::dump($finalServices, 10, 2); - } + $resource->docker_compose_raw = Yaml::dump($yaml, 10, 2); + $resource->docker_compose = Yaml::dump($finalServices, 10, 2); + data_forget($resource, 'environment_variables'); + data_forget($resource, 'environment_variables_preview'); $resource->save(); return collect($finalServices); } } -function parseEnvVariable(Str|string $value) +function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $preview_id = null): Collection { - $value = str($value); - $count = substr_count($value->value(), '_'); - $command = null; - $forService = null; - $generatedValue = null; - $port = null; - if ($value->startsWith('SERVICE')) { - if ($count === 2) { - if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) { - // SERVICE_FQDN_UMAMI - $command = $value->after('SERVICE_')->beforeLast('_'); - $forService = $value->afterLast('_'); - } else { - // SERVICE_BASE64_UMAMI - $command = $value->after('SERVICE_')->beforeLast('_'); + $isApplication = $resource instanceof Application; + $isService = $resource instanceof Service; + + $uuid = data_get($resource, 'uuid'); + $compose = data_get($resource, 'docker_compose_raw'); + if (! $compose) { + return collect([]); + } + + if ($isApplication) { + $nameOfId = 'application_id'; + $pullRequestId = $pull_request_id; + $isPullRequest = $pullRequestId == 0 ? false : true; + $server = data_get($resource, 'destination.server'); + $fileStorages = $resource->fileStorages(); + } elseif ($isService) { + $nameOfId = 'service_id'; + $server = data_get($resource, 'server'); + $allServices = get_service_templates(); + } else { + return collect([]); + } + + try { + $yaml = Yaml::parse($compose); + } catch (\Exception) { + return collect([]); + } + + $services = data_get($yaml, 'services', collect([])); + $topLevel = collect([ + 'volumes' => collect(data_get($yaml, 'volumes', [])), + 'networks' => collect(data_get($yaml, 'networks', [])), + 'configs' => collect(data_get($yaml, 'configs', [])), + 'secrets' => collect(data_get($yaml, 'secrets', [])), + ]); + // If there are predefined volumes, make sure they are not null + if ($topLevel->get('volumes')->count() > 0) { + $temp = collect([]); + foreach ($topLevel['volumes'] as $volumeName => $volume) { + if (is_null($volume)) { + continue; } + $temp->put($volumeName, $volume); } - if ($count === 3) { - if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) { - // SERVICE_FQDN_UMAMI_1000 - $command = $value->after('SERVICE_')->before('_'); - $forService = $value->after('SERVICE_')->after('_')->before('_'); - $port = $value->afterLast('_'); - if (filter_var($port, FILTER_VALIDATE_INT) === false) { - $port = null; + $topLevel['volumes'] = $temp; + } + // Get the base docker network + $baseNetwork = collect([$uuid]); + if ($isApplication && $isPullRequest) { + $baseNetwork = collect(["{$uuid}-{$pullRequestId}"]); + } + + $parsedServices = collect([]); + // ray()->clearAll(); + + $allMagicEnvironments = collect([]); + foreach ($services as $serviceName => $service) { + $predefinedPort = null; + $magicEnvironments = collect([]); + $image = data_get_str($service, 'image'); + $environment = collect(data_get($service, 'environment', [])); + $buildArgs = collect(data_get($service, 'build.args', [])); + $environment = $environment->merge($buildArgs); + $isDatabase = isDatabaseImage(data_get_str($service, 'image')); + + if ($isService) { + $containerName = "$serviceName-{$resource->uuid}"; + + if ($serviceName === 'registry') { + $tempServiceName = 'docker-registry'; + } else { + $tempServiceName = $serviceName; + } + if (str(data_get($service, 'image'))->contains('glitchtip')) { + $tempServiceName = 'glitchtip'; + } + if ($serviceName === 'supabase-kong') { + $tempServiceName = 'supabase'; + } + $serviceDefinition = data_get($allServices, $tempServiceName); + $predefinedPort = data_get($serviceDefinition, 'port'); + if ($serviceName === 'plausible') { + $predefinedPort = '8000'; + } + if ($isDatabase) { + $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first(); + if ($applicationFound) { + $savedService = $applicationFound; + $savedService = ServiceDatabase::firstOrCreate([ + 'name' => $applicationFound->name, + 'image' => $applicationFound->image, + 'service_id' => $applicationFound->service_id, + ]); + $applicationFound->delete(); + } else { + $savedService = ServiceDatabase::firstOrCreate([ + 'name' => $serviceName, + 'image' => $image, + 'service_id' => $resource->id, + ]); } } else { - // SERVICE_BASE64_64_UMAMI - $command = $value->after('SERVICE_')->beforeLast('_'); + $savedService = ServiceApplication::firstOrCreate([ + 'name' => $serviceName, + 'image' => $image, + 'service_id' => $resource->id, + ]); + } + $environment = collect(data_get($service, 'environment', [])); + $buildArgs = collect(data_get($service, 'build.args', [])); + $environment = $environment->merge($buildArgs); + + // convert environment variables to one format + $environment = convertComposeEnvironmentToArray($environment); + + // Add Coolify defined environments + $allEnvironments = $resource->environment_variables()->get(['key', 'value']); + + $allEnvironments = $allEnvironments->mapWithKeys(function ($item) { + return [$item['key'] => $item['value']]; + }); + // filter and add magic environments + foreach ($environment as $key => $value) { + // Get all SERVICE_ variables from keys and values + $key = str($key); + $value = str($value); + + $regex = '/\$(\{?([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\}?)/'; + preg_match_all($regex, $value, $valueMatches); + if (count($valueMatches[1]) > 0) { + foreach ($valueMatches[1] as $match) { + $match = replaceVariables($match); + if ($match->startsWith('SERVICE_')) { + if ($magicEnvironments->has($match->value())) { + continue; + } + $magicEnvironments->put($match->value(), ''); + } + } + } + + // Get magic environments where we need to preset the FQDN + if ($key->startsWith('SERVICE_FQDN_')) { + // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000 + if (substr_count(str($key)->value(), '_') === 3) { + $fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value(); + $port = $key->afterLast('_')->value(); + } else { + $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); + $port = null; + } + if ($isApplication) { + $fqdn = generateFqdn($server, "{$resource->name}-$uuid"); + } elseif ($isService) { + if ($fqdnFor) { + $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); + } else { + $fqdn = generateFqdn($server, "{$savedService->name}-$uuid"); + } + } + + if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) { + $path = $value->value(); + if ($path !== '/') { + $fqdn = "$fqdn$path"; + } + } + $fqdnWithPort = $fqdn; + if ($port) { + $fqdnWithPort = "$fqdn:$port"; + } + if ($isApplication && is_null($resource->fqdn)) { + data_forget($resource, 'environment_variables'); + data_forget($resource, 'environment_variables_preview'); + $resource->fqdn = $fqdnWithPort; + $resource->save(); + } elseif ($isService && is_null($savedService->fqdn)) { + $savedService->fqdn = $fqdnWithPort; + $savedService->save(); + } + + if (substr_count(str($key)->value(), '_') === 2) { + $resource->environment_variables()->where('key', $key->value())->where($nameOfId, $resource->id)->firstOrCreate([ + 'key' => $key->value(), + $nameOfId => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } + if (substr_count(str($key)->value(), '_') === 3) { + $newKey = str($key)->beforeLast('_'); + $resource->environment_variables()->where('key', $newKey->value())->where($nameOfId, $resource->id)->firstOrCreate([ + 'key' => $newKey->value(), + $nameOfId => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } + } + } + + $allMagicEnvironments = $allMagicEnvironments->merge($magicEnvironments); + if ($magicEnvironments->count() > 0) { + foreach ($magicEnvironments as $key => $value) { + $key = str($key); + $value = replaceVariables($value); + $command = parseCommandFromMagicEnvVariable($key); + $found = $resource->environment_variables()->where('key', $key->value())->where($nameOfId, $resource->id)->first(); + if ($found) { + continue; + } + if ($command->value() === 'FQDN') { + $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); + if (str($fqdnFor)->contains('_')) { + $fqdnFor = str($fqdnFor)->before('_'); + } + if ($isApplication) { + $fqdn = generateFqdn($server, "{$resource->name}-$uuid"); + } elseif ($isService) { + $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); + } + $resource->environment_variables()->where('key', $key->value())->where($nameOfId, $resource->id)->firstOrCreate([ + 'key' => $key->value(), + $nameOfId => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } elseif ($command->value() === 'URL') { + $fqdnFor = $key->after('SERVICE_URL_')->lower()->value(); + if (str($fqdnFor)->contains('_')) { + $fqdnFor = str($fqdnFor)->before('_'); + } + if ($isApplication) { + $fqdn = generateFqdn($server, "{$resource->name}-$uuid"); + } elseif ($isService) { + $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); + } + $fqdn = str($fqdn)->replace('http://', '')->replace('https://', ''); + $resource->environment_variables()->where('key', $key->value())->where($nameOfId, $resource->id)->firstOrCreate([ + 'key' => $key->value(), + $nameOfId => $resource->id, + ], [ + 'value' => $fqdn, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } else { + $value = generateEnvValue($command, $resource); + $resource->environment_variables()->where('key', $key->value())->where($nameOfId, $resource->id)->firstOrCreate([ + 'key' => $key->value(), + $nameOfId => $resource->id, + ], [ + 'value' => $value, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } + } } } } + // Parse the rest of the services + foreach ($services as $serviceName => $service) { + $image = data_get_str($service, 'image'); + $restart = data_get_str($service, 'restart', RESTART_MODE); + $logging = data_get($service, 'logging'); + + if ($server->isLogDrainEnabled()) { + if ($resource instanceof Application && $resource->isLogDrainEnabled()) { + $logging = generate_fluentd_configuration(); + } + } + $volumes = collect(data_get($service, 'volumes', [])); + $networks = collect(data_get($service, 'networks', [])); + $use_network_mode = data_get($service, 'network_mode') !== null; + $depends_on = collect(data_get($service, 'depends_on', [])); + $labels = collect(data_get($service, 'labels', [])); + $environment = collect(data_get($service, 'environment', [])); + $ports = collect(data_get($service, 'ports', [])); + $buildArgs = collect(data_get($service, 'build.args', [])); + $environment = $environment->merge($buildArgs); + + $environment = convertComposeEnvironmentToArray($environment); + $coolifyEnvironments = collect([]); + + $isDatabase = isDatabaseImage(data_get_str($service, 'image')); + $volumesParsed = collect([]); + + if ($isApplication) { + $baseName = generateApplicationContainerName( + application: $resource, + pull_request_id: $pullRequestId + ); + $containerName = "$serviceName-$baseName"; + $predefinedPort = null; + } elseif ($isService) { + $containerName = "$serviceName-{$resource->uuid}"; + + if ($serviceName === 'registry') { + $tempServiceName = 'docker-registry'; + } else { + $tempServiceName = $serviceName; + } + if (str(data_get($service, 'image'))->contains('glitchtip')) { + $tempServiceName = 'glitchtip'; + } + if ($serviceName === 'supabase-kong') { + $tempServiceName = 'supabase'; + } + $serviceDefinition = data_get($allServices, $tempServiceName); + $predefinedPort = data_get($serviceDefinition, 'port'); + if ($serviceName === 'plausible') { + $predefinedPort = '8000'; + } + + if ($isDatabase) { + $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first(); + if ($applicationFound) { + $savedService = $applicationFound; + $savedService = ServiceDatabase::firstOrCreate([ + 'name' => $applicationFound->name, + 'image' => $applicationFound->image, + 'service_id' => $applicationFound->service_id, + ]); + $applicationFound->delete(); + } else { + $savedService = ServiceDatabase::firstOrCreate([ + 'name' => $serviceName, + 'image' => $image, + 'service_id' => $resource->id, + ]); + } + } else { + $savedService = ServiceApplication::firstOrCreate([ + 'name' => $serviceName, + 'image' => $image, + 'service_id' => $resource->id, + ]); + } + $fileStorages = $savedService->fileStorages(); + if ($savedService->image !== $image) { + $savedService->image = $image; + $savedService->save(); + } + } + + $originalResource = $isApplication ? $resource : $savedService; + + if ($volumes->count() > 0) { + foreach ($volumes as $index => $volume) { + $type = null; + $source = null; + $target = null; + $content = null; + $isDirectory = false; + if (is_string($volume)) { + $source = str($volume)->before(':'); + $target = str($volume)->after(':')->beforeLast(':'); + $foundConfig = $fileStorages->whereMountPath($target)->first(); + if (sourceIsLocal($source)) { + $type = str('bind'); + if ($foundConfig) { + $contentNotNull_temp = data_get($foundConfig, 'content'); + if ($contentNotNull_temp) { + $content = $contentNotNull_temp; + } + $isDirectory = data_get($foundConfig, 'is_directory'); + } else { + // By default, we cannot determine if the bind is a directory or not, so we set it to directory + $isDirectory = true; + } + } else { + $type = str('volume'); + } + } elseif (is_array($volume)) { + $type = data_get_str($volume, 'type'); + $source = data_get_str($volume, 'source'); + $target = data_get_str($volume, 'target'); + $content = data_get($volume, 'content'); + $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null); + + $foundConfig = $fileStorages->whereMountPath($target)->first(); + if ($foundConfig) { + $contentNotNull_temp = data_get($foundConfig, 'content'); + if ($contentNotNull_temp) { + $content = $contentNotNull_temp; + } + $isDirectory = data_get($foundConfig, 'is_directory'); + } else { + // if isDirectory is not set (or false) & content is also not set, we assume it is a directory + if ((is_null($isDirectory) || ! $isDirectory) && is_null($content)) { + $isDirectory = true; + } + } + } + if ($type->value() === 'bind') { + if ($source->value() === '/var/run/docker.sock') { + $volume = $source->value().':'.$target->value(); + } elseif ($source->value() === '/tmp' || $source->value() === '/tmp/') { + $volume = $source->value().':'.$target->value(); + } else { + if ((int) $resource->compose_parsing_version >= 4) { + if ($isApplication) { + $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); + } elseif ($isService) { + $mainDirectory = str(base_configuration_dir().'/services/'.$uuid); + } + } else { + $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); + } + $source = replaceLocalSource($source, $mainDirectory); + if ($isApplication && $isPullRequest) { + $source = $source."-pr-$pullRequestId"; + } + LocalFileVolume::updateOrCreate( + [ + 'mount_path' => $target, + 'resource_id' => $originalResource->id, + 'resource_type' => get_class($originalResource), + ], + [ + 'fs_path' => $source, + 'mount_path' => $target, + 'content' => $content, + 'is_directory' => $isDirectory, + 'resource_id' => $originalResource->id, + 'resource_type' => get_class($originalResource), + ] + ); + if (isDev()) { + if ((int) $resource->compose_parsing_version >= 4) { + if ($isApplication) { + $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid); + } elseif ($isService) { + $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/services/'.$uuid); + } + } else { + $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid); + } + } + $volume = "$source:$target"; + } + } elseif ($type->value() === 'volume') { + if ($topLevel->get('volumes')->has($source->value())) { + $temp = $topLevel->get('volumes')->get($source->value()); + if (data_get($temp, 'driver_opts.type') === 'cifs') { + continue; + } + if (data_get($temp, 'driver_opts.type') === 'nfs') { + continue; + } + } + $slugWithoutUuid = Str::slug($source, '-'); + $name = "{$uuid}_{$slugWithoutUuid}"; + + if ($isApplication && $isPullRequest) { + $name = "{$name}-pr-$pullRequestId"; + } + if (is_string($volume)) { + $source = str($volume)->before(':'); + $target = str($volume)->after(':')->beforeLast(':'); + $source = $name; + $volume = "$source:$target"; + } elseif (is_array($volume)) { + data_set($volume, 'source', $name); + } + $topLevel->get('volumes')->put($name, [ + 'name' => $name, + ]); + LocalPersistentVolume::updateOrCreate( + [ + 'name' => $name, + 'resource_id' => $originalResource->id, + 'resource_type' => get_class($originalResource), + ], + [ + 'name' => $name, + 'mount_path' => $target, + 'resource_id' => $originalResource->id, + 'resource_type' => get_class($originalResource), + ] + ); + } + dispatch(new ServerFilesFromServerJob($originalResource)); + $volumesParsed->put($index, $volume); + } + } + + if ($depends_on?->count() > 0) { + if ($isApplication && $isPullRequest) { + $newDependsOn = collect([]); + $depends_on->each(function ($dependency, $condition) use ($pullRequestId, $newDependsOn) { + if (is_numeric($condition)) { + $dependency = "$dependency-pr-$pullRequestId"; + + $newDependsOn->put($condition, $dependency); + } else { + $condition = "$condition-pr-$pullRequestId"; + $newDependsOn->put($condition, $dependency); + } + }); + $depends_on = $newDependsOn; + } + } + if (! $use_network_mode) { + if ($topLevel->get('networks')?->count() > 0) { + foreach ($topLevel->get('networks') as $networkName => $network) { + if ($networkName === 'default') { + continue; + } + // ignore aliases + if ($network['aliases'] ?? false) { + continue; + } + $networkExists = $networks->contains(function ($value, $key) use ($networkName) { + return $value == $networkName || $key == $networkName; + }); + if (! $networkExists) { + $networks->put($networkName, null); + } + } + } + $baseNetworkExists = $networks->contains(function ($value, $_) use ($baseNetwork) { + return $value == $baseNetwork; + }); + if (! $baseNetworkExists) { + foreach ($baseNetwork as $network) { + $topLevel->get('networks')->put($network, [ + 'name' => $network, + 'external' => true, + ]); + } + } + } + + // Collect/create/update ports + $collectedPorts = collect([]); + if ($ports->count() > 0) { + foreach ($ports as $sport) { + if (is_string($sport) || is_numeric($sport)) { + $collectedPorts->push($sport); + } + if (is_array($sport)) { + $target = data_get($sport, 'target'); + $published = data_get($sport, 'published'); + $protocol = data_get($sport, 'protocol'); + $collectedPorts->push("$target:$published/$protocol"); + } + } + } + if ($isService) { + $originalResource->ports = $collectedPorts->implode(','); + $originalResource->save(); + } + + $networks_temp = collect(); + + if (! $use_network_mode) { + foreach ($networks as $key => $network) { + if (gettype($network) === 'string') { + // networks: + // - appwrite + $networks_temp->put($network, null); + } elseif (gettype($network) === 'array') { + // networks: + // default: + // ipv4_address: 192.168.203.254 + $networks_temp->put($key, $network); + } + } + foreach ($baseNetwork as $key => $network) { + $networks_temp->put($network, null); + } + + if ($isApplication) { + if (data_get($resource, 'settings.connect_to_docker_network')) { + $network = $resource->destination->network; + $networks_temp->put($network, null); + $topLevel->get('networks')->put($network, [ + 'name' => $network, + 'external' => true, + ]); + } + } + } + + $normalEnvironments = $environment->diffKeys($allMagicEnvironments); + $normalEnvironments = $normalEnvironments->filter(function ($value, $key) { + return ! str($value)->startsWith('SERVICE_'); + }); + + foreach ($normalEnvironments as $key => $value) { + $key = str($key); + $value = str($value); + $originalValue = $value; + $parsedValue = replaceVariables($value); + if ($value->startsWith('$SERVICE_')) { + $resource->environment_variables()->where('key', $key)->where($nameOfId, $resource->id)->firstOrCreate([ + 'key' => $key, + $nameOfId => $resource->id, + ], [ + 'value' => $value, + 'is_build_time' => false, + 'is_preview' => false, + ]); + + continue; + } + if (! $value->startsWith('$')) { + continue; + } + if ($key->value() === $parsedValue->value()) { + $value = null; + $resource->environment_variables()->where('key', $key)->where($nameOfId, $resource->id)->firstOrCreate([ + 'key' => $key, + $nameOfId => $resource->id, + ], [ + 'value' => $value, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } else { + if ($value->startsWith('$')) { + $isRequired = false; + if ($value->contains(':-')) { + $value = replaceVariables($value); + $key = $value->before(':'); + $value = $value->after(':-'); + } elseif ($value->contains('-')) { + $value = replaceVariables($value); + + $key = $value->before('-'); + $value = $value->after('-'); + } elseif ($value->contains(':?')) { + $value = replaceVariables($value); + + $key = $value->before(':'); + $value = $value->after(':?'); + $isRequired = true; + } elseif ($value->contains('?')) { + $value = replaceVariables($value); + + $key = $value->before('?'); + $value = $value->after('?'); + $isRequired = true; + } + if ($originalValue->value() === $value->value()) { + // This means the variable does not have a default value, so it needs to be created in Coolify + $parsedKeyValue = replaceVariables($value); + $resource->environment_variables()->where('key', $parsedKeyValue)->where($nameOfId, $resource->id)->firstOrCreate([ + 'key' => $parsedKeyValue, + $nameOfId => $resource->id, + ], [ + 'is_build_time' => false, + 'is_preview' => false, + 'is_required' => $isRequired, + ]); + // Add the variable to the environment so it will be shown in the deployable compose file + $environment[$parsedKeyValue->value()] = $resource->environment_variables()->where('key', $parsedKeyValue)->where($nameOfId, $resource->id)->first()->value; + + continue; + } + $resource->environment_variables()->where('key', $key)->where($nameOfId, $resource->id)->firstOrCreate([ + 'key' => $key, + $nameOfId => $resource->id, + ], [ + 'value' => $value, + 'is_build_time' => false, + 'is_preview' => false, + 'is_required' => $isRequired, + ]); + } + } + } + if ($isApplication) { + $branch = $originalResource->git_branch; + if ($pullRequestId !== 0) { + $branch = "pull/{$pullRequestId}/head"; + } + if ($originalResource->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { + $coolifyEnvironments->put('COOLIFY_BRANCH', "\"{$branch}\""); + } + } + + // Add COOLIFY_CONTAINER_NAME to environment + if ($resource->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { + $coolifyEnvironments->put('COOLIFY_CONTAINER_NAME', "\"{$containerName}\""); + } + + if ($isApplication) { + $domains = collect(json_decode($resource->docker_compose_domains)) ?? collect([]); + $fqdns = data_get($domains, "$serviceName.domain"); + if ($fqdns) { + $fqdns = str($fqdns)->explode(','); + if ($isPullRequest) { + $preview = $resource->previews()->find($preview_id); + $docker_compose_domains = collect(json_decode(data_get($preview, 'docker_compose_domains'))); + if ($docker_compose_domains->count() > 0) { + $found_fqdn = data_get($docker_compose_domains, "$serviceName.domain"); + if ($found_fqdn) { + $fqdns = collect($found_fqdn); + } else { + $fqdns = collect([]); + } + } else { + $fqdns = $fqdns->map(function ($fqdn) use ($pullRequestId, $resource) { + $preview = ApplicationPreview::findPreviewByApplicationAndPullId($resource->id, $pullRequestId); + $url = Url::fromString($fqdn); + $template = $resource->preview_url_template; + $host = $url->getHost(); + $schema = $url->getScheme(); + $random = new Cuid2; + $preview_fqdn = str_replace('{{random}}', $random, $template); + $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); + $preview_fqdn = str_replace('{{pr_id}}', $pullRequestId, $preview_fqdn); + $preview_fqdn = "$schema://$preview_fqdn"; + $preview->fqdn = $preview_fqdn; + $preview->save(); + + return $preview_fqdn; + }); + } + } + } + $defaultLabels = defaultLabels( + id: $resource->id, + name: $containerName, + pull_request_id: $pullRequestId, + type: 'application' + ); + } elseif ($isService) { + if ($savedService->serviceType()) { + $fqdns = generateServiceSpecificFqdns($savedService); + } else { + $fqdns = collect(data_get($savedService, 'fqdns'))->filter(); + } + $defaultLabels = defaultLabels($resource->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id); + } + // Add COOLIFY_FQDN & COOLIFY_URL to environment + if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { + $coolifyEnvironments->put('COOLIFY_URL', $fqdns->implode(',')); + + $urls = $fqdns->map(function ($fqdn) { + return str($fqdn)->replace('http://', '')->replace('https://', ''); + }); + $coolifyEnvironments->put('COOLIFY_FQDN', $urls->implode(',')); + } + add_coolify_default_environment_variables($resource, $coolifyEnvironments, $resource->environment_variables); + + if ($environment->count() > 0) { + $environment = $environment->filter(function ($value, $key) { + return ! str($key)->startsWith('SERVICE_FQDN_'); + })->map(function ($value, $key) use ($resource) { + // if value is empty, set it to null so if you set the environment variable in the .env file (Coolify's UI), it will used + if (str($value)->isEmpty()) { + if ($resource->environment_variables()->where('key', $key)->exists()) { + $value = $resource->environment_variables()->where('key', $key)->first()->value; + } else { + $value = null; + } + } + + return $value; + }); + } + $serviceLabels = $labels->merge($defaultLabels); + if ($serviceLabels->count() > 0) { + if ($isApplication) { + $isContainerLabelEscapeEnabled = data_get($resource, 'settings.is_container_label_escape_enabled'); + } else { + $isContainerLabelEscapeEnabled = data_get($resource, 'is_container_label_escape_enabled'); + } + if ($isContainerLabelEscapeEnabled) { + $serviceLabels = $serviceLabels->map(function ($value, $key) { + return escapeDollarSign($value); + }); + } + } + if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { + if ($isApplication) { + $shouldGenerateLabelsExactly = $resource->destination->server->settings->generate_exact_labels; + $uuid = $resource->uuid; + $network = data_get($resource, 'destination.network'); + if ($isPullRequest) { + $uuid = "{$resource->uuid}-{$pullRequestId}"; + } + if ($isPullRequest) { + $network = "{$resource->destination->network}-{$pullRequestId}"; + } + } else { + $shouldGenerateLabelsExactly = $resource->server->settings->generate_exact_labels; + $uuid = $resource->uuid; + $network = data_get($resource, 'destination.network'); + } + if ($shouldGenerateLabelsExactly) { + switch ($server->proxyType()) { + case ProxyTypes::TRAEFIK->value: + $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( + uuid: $uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $originalResource->isGzipEnabled(), + is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), + service_name: $serviceName, + image: $image + )); + break; + case ProxyTypes::CADDY->value: + $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( + network: $network, + uuid: $uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $originalResource->isGzipEnabled(), + is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), + service_name: $serviceName, + image: $image, + predefinedPort: $predefinedPort + )); + break; + } + } else { + $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( + uuid: $uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $originalResource->isGzipEnabled(), + is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), + service_name: $serviceName, + image: $image + )); + $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( + network: $network, + uuid: $uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $originalResource->isGzipEnabled(), + is_stripprefix_enabled: $originalResource->isStripprefixEnabled(), + service_name: $serviceName, + image: $image, + predefinedPort: $predefinedPort + )); + } + } + if ($isService) { + if (data_get($service, 'restart') === 'no' || data_get($service, 'exclude_from_hc')) { + $savedService->update(['exclude_from_status' => true]); + } + } + data_forget($service, 'volumes.*.content'); + data_forget($service, 'volumes.*.isDirectory'); + data_forget($service, 'volumes.*.is_directory'); + data_forget($service, 'exclude_from_hc'); + + $volumesParsed = $volumesParsed->map(function ($volume) { + data_forget($volume, 'content'); + data_forget($volume, 'is_directory'); + data_forget($volume, 'isDirectory'); + + return $volume; + }); + + $payload = collect($service)->merge([ + 'container_name' => $containerName, + 'restart' => $restart->value(), + 'labels' => $serviceLabels, + ]); + if (! $use_network_mode) { + $payload['networks'] = $networks_temp; + } + if ($ports->count() > 0) { + $payload['ports'] = $ports; + } + if ($volumesParsed->count() > 0) { + $payload['volumes'] = $volumesParsed; + } + if ($environment->count() > 0 || $coolifyEnvironments->count() > 0) { + $payload['environment'] = $environment->merge($coolifyEnvironments); + } + if ($logging) { + $payload['logging'] = $logging; + } + if ($depends_on->count() > 0) { + $payload['depends_on'] = $depends_on; + } + if ($isApplication && $isPullRequest) { + $serviceName = "{$serviceName}-pr-{$pullRequestId}"; + } + + $parsedServices->put($serviceName, $payload); + } + $topLevel->put('services', $parsedServices); + + $customOrder = ['services', 'volumes', 'networks', 'configs', 'secrets']; + + $topLevel = $topLevel->sortBy(function ($value, $key) use ($customOrder) { + return array_search($key, $customOrder); + }); + + $resource->docker_compose = Yaml::dump(convertToArray($topLevel), 10, 2); + data_forget($resource, 'environment_variables'); + data_forget($resource, 'environment_variables_preview'); + $resource->save(); + + return $topLevel; +} + +function generate_fluentd_configuration(): array +{ + return [ + 'driver' => 'fluentd', + 'options' => [ + 'fluentd-address' => 'tcp://127.0.0.1:24224', + 'fluentd-async' => 'true', + 'fluentd-sub-second-precision' => 'true', + // env vars are used in the LogDrain configurations + 'env' => 'COOLIFY_APP_NAME,COOLIFY_PROJECT_NAME,COOLIFY_SERVER_IP,COOLIFY_ENVIRONMENT_NAME', + ], + ]; +} + +function isAssociativeArray($array) +{ + if ($array instanceof Collection) { + $array = $array->toArray(); + } + + if (! is_array($array)) { + throw new \InvalidArgumentException('Input must be an array or a Collection.'); + } + + if ($array === []) { + return false; + } + + return array_keys($array) !== range(0, count($array) - 1); +} + +/** + * This method adds the default environment variables to the resource. + * - COOLIFY_APP_NAME + * - COOLIFY_PROJECT_NAME + * - COOLIFY_SERVER_IP + * - COOLIFY_ENVIRONMENT_NAME + * + * Theses variables are added in place to the $where_to_add array. + */ +function add_coolify_default_environment_variables(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|Application|Service $resource, Collection &$where_to_add, ?Collection $where_to_check = null) +{ + // Currently disabled + return; + if ($resource instanceof Service) { + $ip = $resource->server->ip; + } else { + $ip = $resource->destination->server->ip; + } + if (isAssociativeArray($where_to_add)) { + $isAssociativeArray = true; + } else { + $isAssociativeArray = false; + } + if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_APP_NAME')->isEmpty()) { + if ($isAssociativeArray) { + $where_to_add->put('COOLIFY_APP_NAME', "\"{$resource->name}\""); + } else { + $where_to_add->push("COOLIFY_APP_NAME=\"{$resource->name}\""); + } + } + if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_SERVER_IP')->isEmpty()) { + if ($isAssociativeArray) { + $where_to_add->put('COOLIFY_SERVER_IP', "\"{$ip}\""); + } else { + $where_to_add->push("COOLIFY_SERVER_IP=\"{$ip}\""); + } + } + if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_ENVIRONMENT_NAME')->isEmpty()) { + if ($isAssociativeArray) { + $where_to_add->put('COOLIFY_ENVIRONMENT_NAME', "\"{$resource->environment->name}\""); + } else { + $where_to_add->push("COOLIFY_ENVIRONMENT_NAME=\"{$resource->environment->name}\""); + } + } + if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_PROJECT_NAME')->isEmpty()) { + if ($isAssociativeArray) { + $where_to_add->put('COOLIFY_PROJECT_NAME', "\"{$resource->project()->name}\""); + } else { + $where_to_add->push("COOLIFY_PROJECT_NAME=\"{$resource->project()->name}\""); + } + } +} + +function convertComposeEnvironmentToArray($environment) +{ + $convertedServiceVariables = collect([]); + if (isAssociativeArray($environment)) { + // Example: $environment = ['FOO' => 'bar', 'BAZ' => 'qux']; + if ($environment instanceof Collection) { + $changedEnvironment = collect([]); + $environment->each(function ($value, $key) use ($changedEnvironment) { + if (is_numeric($key)) { + $parts = explode('=', $value, 2); + if (count($parts) === 2) { + $key = $parts[0]; + $realValue = $parts[1] ?? ''; + $changedEnvironment->put($key, $realValue); + } else { + $changedEnvironment->put($key, $value); + } + } else { + $changedEnvironment->put($key, $value); + } + }); + + return $changedEnvironment; + } + $convertedServiceVariables = $environment; + } else { + // Example: $environment = ['FOO=bar', 'BAZ=qux']; + foreach ($environment as $value) { + if (is_string($value)) { + $parts = explode('=', $value, 2); + $key = $parts[0]; + $realValue = $parts[1] ?? ''; + if ($key) { + $convertedServiceVariables->put($key, $realValue); + } + } + } + } + + return $convertedServiceVariables; +} +function instanceSettings() +{ + return InstanceSettings::get(); +} + +function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id) +{ + $server = Server::find($server_id)->where('team_id', $team_id)->first(); + if (! $server) { + return; + } + $uuid = new Cuid2; + $cloneCommand = "git clone --no-checkout -b $branch $repository ."; + $workdir = rtrim($base_directory, '/'); + $fileList = collect([".$workdir/coolify.json"]); + $commands = collect([ + "rm -rf /tmp/{$uuid}", + "mkdir -p /tmp/{$uuid}", + "cd /tmp/{$uuid}", + $cloneCommand, + 'git sparse-checkout init --cone', + "git sparse-checkout set {$fileList->implode(' ')}", + 'git read-tree -mu HEAD', + "cat .$workdir/coolify.json", + 'rm -rf /tmp/{$uuid}', + ]); + try { + return instant_remote_process($commands, $server); + } catch (\Exception) { + // continue + } +} + +function loggy($message = null, array $context = []) +{ + if (! isDev()) { + return; + } + if (function_exists('ray') && config('app.debug')) { + ray($message, $context); + } + if (is_null($message)) { + return app('log'); + } + + return app('log')->debug($message, $context); +} +function sslipDomainWarning(string $domains) +{ + $domains = str($domains)->trim()->explode(','); + $showSslipHttpsWarning = false; + $domains->each(function ($domain) use (&$showSslipHttpsWarning) { + if (str($domain)->contains('https') && str($domain)->contains('sslip')) { + $showSslipHttpsWarning = true; + } + }); + + return $showSslipHttpsWarning; +} + +function isEmailRateLimited(string $limiterKey, int $decaySeconds = 3600, ?callable $callbackOnSuccess = null): bool +{ + if (isDev()) { + $decaySeconds = 120; + } + $rateLimited = false; + $executed = RateLimiter::attempt( + $limiterKey, + $maxAttempts = 0, + function () use (&$rateLimited, &$limiterKey, $callbackOnSuccess) { + isDev() && loggy('Rate limit not reached for '.$limiterKey); + $rateLimited = false; + + if ($callbackOnSuccess) { + $callbackOnSuccess(); + } + }, + $decaySeconds, + ); + if (! $executed) { + isDev() && loggy('Rate limit reached for '.$limiterKey.'. Rate limiter will be disabled for '.$decaySeconds.' seconds.'); + $rateLimited = true; + } + + return $rateLimited; +} + +function defaultNginxConfiguration(): string +{ + return 'server { + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/index.html =404; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + try_files $uri @redirect_to_index; + internal; + } + + error_page 404 = @handle_404; + + location @handle_404 { + root /usr/share/nginx/html; + try_files /404.html @redirect_to_index; + internal; + } + + location @redirect_to_index { + return 302 /; + } +}'; +} + +function convertGitUrl(string $gitRepository, string $deploymentType, ?GithubApp $source = null): array +{ + $repository = $gitRepository; + $providerInfo = [ + 'host' => null, + 'user' => 'git', + 'port' => 22, + 'repository' => $gitRepository, + ]; + $sshMatches = []; + $matches = []; + + // Let's try and parse the string to detect if it's a valid SSH string or not + preg_match('/((.*?)\:\/\/)?(.*@.*:.*)/', $gitRepository, $sshMatches); + + if ($deploymentType === 'deploy_key' && empty($sshMatches) && $source) { + // If this happens, the user may have provided an HTTP URL when they needed an SSH one + // Let's try and fix that for known Git providers + switch ($source->getMorphClass()) { + case \App\Models\GithubApp::class: + $providerInfo['host'] = Url::fromString($source->html_url)->getHost(); + $providerInfo['port'] = $source->custom_port; + $providerInfo['user'] = $source->custom_user; + break; + } + if (! empty($providerInfo['host'])) { + // Until we do not support more providers with App (like GithubApp), this will be always true, port will be 22 + if ($providerInfo['port'] === 22) { + $repository = "{$providerInfo['user']}@{$providerInfo['host']}:{$providerInfo['repository']}"; + } else { + $repository = "ssh://{$providerInfo['user']}@{$providerInfo['host']}:{$providerInfo['port']}/{$providerInfo['repository']}"; + } + } + } + + preg_match('/(?<=:)\d+(?=\/)/', $gitRepository, $matches); + + if (count($matches) === 1) { + $providerInfo['port'] = $matches[0]; + $gitHost = str($gitRepository)->before(':'); + $gitRepo = str($gitRepository)->after('/'); + $repository = "$gitHost:$gitRepo"; + } + return [ - 'command' => $command, - 'forService' => $forService, - 'generatedValue' => $generatedValue, - 'port' => $port, + 'repository' => $repository, + 'port' => $providerInfo['port'], ]; } -function generateEnvValue(string $command, ?Service $service = null) -{ - switch ($command) { - case 'PASSWORD': - $generatedValue = Str::password(symbols: false); - break; - case 'PASSWORD_64': - $generatedValue = Str::password(length: 64, symbols: false); - break; - // This is not base64, it's just a random string - case 'BASE64_64': - $generatedValue = Str::random(64); - break; - case 'BASE64_128': - $generatedValue = Str::random(128); - break; - case 'BASE64': - case 'BASE64_32': - $generatedValue = Str::random(32); - break; - // This is base64, - case 'REALBASE64_64': - $generatedValue = base64_encode(Str::random(64)); - break; - case 'REALBASE64_128': - $generatedValue = base64_encode(Str::random(128)); - break; - case 'REALBASE64': - case 'REALBASE64_32': - $generatedValue = base64_encode(Str::random(32)); - break; - case 'USER': - $generatedValue = Str::random(16); - break; - case 'SUPABASEANON': - $signingKey = $service->environment_variables()->where('key', 'SERVICE_PASSWORD_JWT')->first(); - if (is_null($signingKey)) { - return; - } else { - $signingKey = $signingKey->value; - } - $key = InMemory::plainText($signingKey); - $algorithm = new Sha256(); - $tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default())); - $now = new DateTimeImmutable(); - $now = $now->setTime($now->format('H'), $now->format('i')); - $token = $tokenBuilder - ->issuedBy('supabase') - ->issuedAt($now) - ->expiresAt($now->modify('+100 year')) - ->withClaim('role', 'anon') - ->getToken($algorithm, $key); - $generatedValue = $token->toString(); - break; - case 'SUPABASESERVICE': - $signingKey = $service->environment_variables()->where('key', 'SERVICE_PASSWORD_JWT')->first(); - if (is_null($signingKey)) { - return; - } else { - $signingKey = $signingKey->value; - } - $key = InMemory::plainText($signingKey); - $algorithm = new Sha256(); - $tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default())); - $now = new DateTimeImmutable(); - $now = $now->setTime($now->format('H'), $now->format('i')); - $token = $tokenBuilder - ->issuedBy('supabase') - ->issuedAt($now) - ->expiresAt($now->modify('+100 year')) - ->withClaim('role', 'service_role') - ->getToken($algorithm, $key); - $generatedValue = $token->toString(); - break; - default: - $generatedValue = Str::random(16); - break; - } - - return $generatedValue; -} - -function getRealtime() -{ - $envDefined = env('PUSHER_PORT'); - if (empty($envDefined)) { - $url = Url::fromString(Request::getSchemeAndHttpHost()); - $port = $url->getPort(); - if ($port) { - return '6001'; - } else { - return null; - } - } else { - return $envDefined; - } -} - -function validate_dns_entry(string $fqdn, Server $server) -{ - // https://www.cloudflare.com/ips-v4/# - $cloudflare_ips = collect(['173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20', '197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/13', '172.64.0.0/13', '131.0.72.0/22']); - - $url = Url::fromString($fqdn); - $host = $url->getHost(); - if (str($host)->contains('sslip.io')) { - return true; - } - $settings = InstanceSettings::get(); - $is_dns_validation_enabled = data_get($settings, 'is_dns_validation_enabled'); - if (! $is_dns_validation_enabled) { - return true; - } - $dns_servers = data_get($settings, 'custom_dns_servers'); - $dns_servers = str($dns_servers)->explode(','); - if ($server->id === 0) { - $ip = data_get($settings, 'public_ipv4', data_get($settings, 'public_ipv6', $server->ip)); - } else { - $ip = $server->ip; - } - $found_matching_ip = false; - $type = \PurplePixie\PhpDns\DNSTypes::NAME_A; - foreach ($dns_servers as $dns_server) { - try { - ray("Checking $host on $dns_server"); - $query = new DNSQuery($dns_server); - $results = $query->query($host, $type); - if ($results === false || $query->hasError()) { - ray('Error: '.$query->getLasterror()); - } else { - foreach ($results as $result) { - if ($result->getType() == $type) { - if (ip_match($result->getData(), $cloudflare_ips->toArray(), $match)) { - ray("Found match in Cloudflare IPs: $match"); - $found_matching_ip = true; - break; - } - if ($result->getData() === $ip) { - ray($host.' has IP address '.$result->getData()); - ray($result->getString()); - $found_matching_ip = true; - break; - } - } - } - } - } catch (\Exception $e) { - } - } - ray("Found match: $found_matching_ip"); - - return $found_matching_ip; -} - -function ip_match($ip, $cidrs, &$match = null) -{ - foreach ((array) $cidrs as $cidr) { - [$subnet, $mask] = explode('/', $cidr); - if (((ip2long($ip) & ($mask = ~((1 << (32 - $mask)) - 1))) == (ip2long($subnet) & $mask))) { - $match = $cidr; - - return true; - } - } - - return false; -} -function check_domain_usage(ServiceApplication|Application|null $resource = null, ?string $domain = null) -{ - if ($resource) { - if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose') { - $domains = data_get(json_decode($resource->docker_compose_domains, true), '*.domain'); - ray($domains); - $domains = collect($domains); - } else { - $domains = collect($resource->fqdns); - } - } elseif ($domain) { - $domains = collect($domain); - } else { - throw new \RuntimeException('No resource or FQDN provided.'); - } - $domains = $domains->map(function ($domain) { - if (str($domain)->endsWith('/')) { - $domain = str($domain)->beforeLast('/'); - } - - return str($domain); - }); - $apps = Application::all(); - foreach ($apps as $app) { - $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); - foreach ($list_of_domains as $domain) { - if (str($domain)->endsWith('/')) { - $domain = str($domain)->beforeLast('/'); - } - $naked_domain = str($domain)->value(); - if ($domains->contains($naked_domain)) { - if (data_get($resource, 'uuid')) { - if ($resource->uuid !== $app->uuid) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:

{$app->name}."); - } - } elseif ($domain) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:

{$app->name}."); - } - } - } - } - $apps = ServiceApplication::all(); - foreach ($apps as $app) { - $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); - foreach ($list_of_domains as $domain) { - if (str($domain)->endsWith('/')) { - $domain = str($domain)->beforeLast('/'); - } - $naked_domain = str($domain)->value(); - if ($domains->contains($naked_domain)) { - if (data_get($resource, 'uuid')) { - if ($resource->uuid !== $app->uuid) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:

{$app->name}."); - } - } elseif ($domain) { - throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:

{$app->name}."); - } - } - } - } - if ($resource) { - $settings = InstanceSettings::get(); - if (data_get($settings, 'fqdn')) { - $domain = data_get($settings, 'fqdn'); - if (str($domain)->endsWith('/')) { - $domain = str($domain)->beforeLast('/'); - } - $naked_domain = str($domain)->value(); - if ($domains->contains($naked_domain)) { - throw new \RuntimeException("Domain $naked_domain is already in use by this Coolify instance."); - } - } - } -} - -function parseCommandsByLineForSudo(Collection $commands, Server $server): array -{ - $commands = $commands->map(function ($line) { - if (! str($line)->startsWith('cd') && ! str($line)->startsWith('command') && ! str($line)->startsWith('echo') && ! str($line)->startsWith('true')) { - return "sudo $line"; - } - - return $line; - }); - $commands = $commands->map(function ($line) use ($server) { - if (Str::startsWith($line, 'sudo mkdir -p')) { - return "$line && sudo chown -R $server->user:$server->user ".Str::after($line, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($line, 'sudo mkdir -p'); - } - - return $line; - }); - $commands = $commands->map(function ($line) { - $line = str($line); - if (str($line)->contains('$(')) { - $line = $line->replace('$(', '$(sudo '); - } - if (str($line)->contains('||')) { - $line = $line->replace('||', '|| sudo'); - } - if (str($line)->contains('&&')) { - $line = $line->replace('&&', '&& sudo'); - } - if (str($line)->contains(' | ')) { - $line = $line->replace(' | ', ' | sudo '); - } - - return $line->value(); - }); - - return $commands->toArray(); -} -function parseLineForSudo(string $command, Server $server): string -{ - if (! str($command)->startSwith('cd') && ! str($command)->startSwith('command')) { - $command = "sudo $command"; - } - if (Str::startsWith($command, 'sudo mkdir -p')) { - $command = "$command && sudo chown -R $server->user:$server->user ".Str::after($command, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($command, 'sudo mkdir -p'); - } - if (str($command)->contains('$(') || str($command)->contains('`')) { - $command = str($command)->replace('$(', '$(sudo ')->replace('`', '`sudo ')->value(); - } - if (str($command)->contains('||')) { - $command = str($command)->replace('||', '|| sudo ')->value(); - } - if (str($command)->contains('&&')) { - $command = str($command)->replace('&&', '&& sudo ')->value(); - } - - return $command; -} - -function get_public_ips() -{ - try { - echo "Refreshing public ips!\n"; - $settings = InstanceSettings::get(); - [$first, $second] = Process::concurrently(function (Pool $pool) { - $pool->path(__DIR__)->command('curl -4s https://ifconfig.io'); - $pool->path(__DIR__)->command('curl -6s https://ifconfig.io'); - }); - $ipv4 = $first->output(); - if ($ipv4) { - $ipv4 = trim($ipv4); - $validate_ipv4 = filter_var($ipv4, FILTER_VALIDATE_IP); - if ($validate_ipv4 == false) { - echo "Invalid ipv4: $ipv4\n"; - - return; - } - $settings->update(['public_ipv4' => $ipv4]); - } - $ipv6 = $second->output(); - if ($ipv6) { - $ipv6 = trim($ipv6); - $validate_ipv6 = filter_var($ipv6, FILTER_VALIDATE_IP); - if ($validate_ipv6 == false) { - echo "Invalid ipv6: $ipv6\n"; - - return; - } - $settings->update(['public_ipv6' => $ipv6]); - } - } catch (\Throwable $e) { - echo "Error: {$e->getMessage()}\n"; - } -} - -function isAnyDeploymentInprogress() -{ - // Only use it in the deployment script - $count = ApplicationDeploymentQueue::whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS, ApplicationDeploymentStatus::QUEUED])->count(); - if ($count > 0) { - echo "There are $count deployments in progress. Exiting...\n"; - exit(1); - } - echo "No deployments in progress.\n"; - exit(0); -} diff --git a/bootstrap/helpers/socialite.php b/bootstrap/helpers/socialite.php index a23dc24d3..cad9de7fa 100644 --- a/bootstrap/helpers/socialite.php +++ b/bootstrap/helpers/socialite.php @@ -7,7 +7,7 @@ function get_socialite_provider(string $provider) { $oauth_setting = OauthSetting::firstWhere('provider', $provider); - if ($provider == 'azure') { + if ($provider === 'azure') { $azure_config = new \SocialiteProviders\Manager\Config( $oauth_setting->client_id, $oauth_setting->client_secret, diff --git a/bootstrap/helpers/subscriptions.php b/bootstrap/helpers/subscriptions.php index 224a65f0a..8ddb1331c 100644 --- a/bootstrap/helpers/subscriptions.php +++ b/bootstrap/helpers/subscriptions.php @@ -1,51 +1,8 @@ user()->id; - $team_id = currentTeam()->id ?? null; - $email = auth()->user()->email ?? null; - $name = auth()->user()->name ?? null; - $url = "https://store.coollabs.io/checkout/buy/$checkout_id?"; - if ($user_id) { - $url .= "&checkout[custom][user_id]={$user_id}"; - } - if (isset($team_id)) { - $url .= "&checkout[custom][team_id]={$team_id}"; - } - if ($email) { - $url .= "&checkout[email]={$email}"; - } - if ($name) { - $url .= "&checkout[name]={$name}"; - } - - return $url; -} - -function getPaymentLink() -{ - return currentTeam()->subscription->lemon_update_payment_menthod_url; -} - -function getRenewDate() -{ - return Carbon::parse(currentTeam()->subscription->lemon_renews_at)->format('Y-M-d H:i:s'); -} - -function getEndDate() -{ - return Carbon::parse(currentTeam()->subscription->lemon_renews_at)->format('Y-M-d H:i:s'); -} - function isSubscriptionActive() { if (! isCloud()) { @@ -60,12 +17,6 @@ function isSubscriptionActive() if (is_null($subscription)) { return false; } - if (isLemon()) { - return $subscription->lemon_status === 'active'; - } - // if (isPaddle()) { - // return $subscription->paddle_status === 'active'; - // } if (isStripe()) { return $subscription->stripe_invoice_paid === true; } @@ -82,12 +33,6 @@ function isSubscriptionOnGracePeriod() if (! $subscription) { return false; } - if (isLemon()) { - $is_still_grace_period = $subscription->lemon_ends_at && - Carbon::parse($subscription->lemon_ends_at) > Carbon::now(); - - return $is_still_grace_period; - } if (isStripe()) { return $subscription->stripe_cancel_at_period_end; } @@ -98,18 +43,10 @@ function subscriptionProvider() { return config('subscription.provider'); } -function isLemon() -{ - return config('subscription.provider') === 'lemon'; -} function isStripe() { return config('subscription.provider') === 'stripe'; } -function isPaddle() -{ - return config('subscription.provider') === 'paddle'; -} function getStripeCustomerPortalSession(Team $team) { Stripe::setApiKey(config('subscription.stripe_api_key')); @@ -118,12 +55,11 @@ function getStripeCustomerPortalSession(Team $team) if (! $stripe_customer_id) { return null; } - $session = \Stripe\BillingPortal\Session::create([ + + return \Stripe\BillingPortal\Session::create([ 'customer' => $stripe_customer_id, 'return_url' => $return_url, ]); - - return $session; } function allowedPathsForUnsubscribedAccounts() { diff --git a/composer.json b/composer.json index b49f9668a..2bae1149c 100644 --- a/composer.json +++ b/composer.json @@ -1,38 +1,43 @@ { - "name": "laravel/laravel", + "name": "coollabsio/coolify", + "description": "The Coolify project.", + "license": "Apache-2.0", "type": "project", - "description": "The Laravel Framework.", "keywords": [ - "framework", - "laravel" + "coolify", + "deployment", + "docker", + "self-hosted", + "server" ], - "license": "MIT", "require": { "php": "^8.2", "danharrin/livewire-rate-limiting": "^1.1", "doctrine/dbal": "^3.6", "guzzlehttp/guzzle": "^7.5.0", - "laravel/fortify": "^v1.16.0", - "laravel/framework": "^v10.7.1", - "laravel/horizon": "^5.23.1", + "laravel/fortify": "^1.16.0", + "laravel/framework": "^11", + "laravel/horizon": "^5.29.1", + "laravel/pail": "^1.1", "laravel/prompts": "^0.1.6", - "laravel/sanctum": "^v3.2.1", - "laravel/socialite": "^v5.14.0", - "laravel/tinker": "^v2.8.1", + "laravel/sanctum": "^4.0", + "laravel/socialite": "^5.14.0", + "laravel/tinker": "^2.8.1", "laravel/ui": "^4.2", "lcobucci/jwt": "^5.0.0", "league/flysystem-aws-s3-v3": "^3.0", "league/flysystem-sftp-v3": "^3.0", "livewire/livewire": "3.4.9", + "log1x/laravel-webfonts": "^1.0", "lorisleiva/laravel-actions": "^2.7", "nubs/random-name-generator": "^2.2", - "phpseclib/phpseclib": "~3.0", + "phpseclib/phpseclib": "^3.0", "pion/laravel-chunk-upload": "^1.5", "poliander/cron": "^3.0", "purplepixie/phpdns": "^2.1", "pusher/pusher-php-server": "^7.2", - "resend/resend-laravel": "^0.5.0", - "sentry/sentry-laravel": "^3.4", + "resend/resend-laravel": "^0.13.0", + "sentry/sentry-laravel": "^4.6", "socialiteproviders/microsoft-azure": "^5.1", "spatie/laravel-activitylog": "^4.7.3", "spatie/laravel-data": "^3.4.3", @@ -42,67 +47,76 @@ "stripe/stripe-php": "^12.0", "symfony/yaml": "^6.2", "visus/cuid2": "^2.0.0", - "yosymfony/toml": "^1.0" + "yosymfony/toml": "^1.0", + "zircote/swagger-php": "^4.10" }, "require-dev": { - "fakerphp/faker": "^v1.21.0", - "laravel/dusk": "^v7.7.0", + "barryvdh/laravel-debugbar": "^3.13", + "fakerphp/faker": "^1.21.0", + "laravel/dusk": "^8.0", "laravel/pint": "^1.16", + "laravel/telescope": "^5.2", "mockery/mockery": "^1.5.1", - "nunomaduro/collision": "^v7.4.0", + "nunomaduro/collision": "^8.1", "pestphp/pest": "^2.16", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^10.0.19", - "serversideup/spin": "^v1.1.0", + "serversideup/spin": "^1.1.0", "spatie/laravel-ignition": "^2.1.0", "symfony/http-client": "^6.2" }, + "minimum-stability": "stable", + "prefer-stable": true, "autoload": { - "files": [ - "bootstrap/includeHelpers.php" - ], "psr-4": { "App\\": "app/", "Database\\Factories\\": "database/factories/", "Database\\Seeders\\": "database/seeders/" - } + }, + "files": [ + "bootstrap/includeHelpers.php" + ] }, "autoload-dev": { "psr-4": { "Tests\\": "tests/" } }, + "config": { + "allow-plugins": { + "pestphp/pest-plugin": true, + "php-http/discovery": true + }, + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true + }, + "extra": { + "laravel": { + "dont-discover": [ + "laravel/telescope" + ] + } + }, "scripts": { - "post-autoload-dump": [ - "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", - "@php artisan package:discover --ansi" + "post-install-cmd": [ + "cp -r 'hooks/' '.git/hooks/'", + "php -r \"copy('hooks/pre-commit', '.git/hooks/pre-commit');\"", + "php -r \"chmod('.git/hooks/pre-commit', 0777);\"" ], "post-update-cmd": [ "@php artisan vendor:publish --tag=laravel-assets --ansi --force", "Illuminate\\Foundation\\ComposerScripts::postUpdate" ], - "post-install-cmd": [], + "post-autoload-dump": [ + "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", + "@php artisan package:discover --ansi" + ], "post-root-package-install": [ "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" ], "post-create-project-cmd": [ "@php artisan key:generate --ansi" ] - }, - "extra": { - "laravel": { - "dont-discover": [] - } - }, - "config": { - "optimize-autoloader": true, - "preferred-install": "dist", - "sort-packages": true, - "allow-plugins": { - "pestphp/pest-plugin": true, - "php-http/discovery": true - } - }, - "minimum-stability": "stable", - "prefer-stable": true -} + } +} \ No newline at end of file diff --git a/composer.lock b/composer.lock index 9d04e9ec7..5eb03b5fc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "dbce9f366320f4d58392673fe25c69f6", + "content-hash": "3f2342fe6b1ba920c8875f8a8fe41962", "packages": [ { "name": "amphp/amp", @@ -229,16 +229,16 @@ }, { "name": "amphp/dns", - "version": "v2.1.2", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/amphp/dns.git", - "reference": "04c88e67bef804203df934703bd422ea72f46b0e" + "reference": "758266b0ea7470e2e42cd098493bc6d6c7100cf7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/dns/zipball/04c88e67bef804203df934703bd422ea72f46b0e", - "reference": "04c88e67bef804203df934703bd422ea72f46b0e", + "url": "https://api.github.com/repos/amphp/dns/zipball/758266b0ea7470e2e42cd098493bc6d6c7100cf7", + "reference": "758266b0ea7470e2e42cd098493bc6d6c7100cf7", "shasum": "" }, "require": { @@ -305,7 +305,7 @@ ], "support": { "issues": "https://github.com/amphp/dns/issues", - "source": "https://github.com/amphp/dns/tree/v2.1.2" + "source": "https://github.com/amphp/dns/tree/v2.2.0" }, "funding": [ { @@ -313,20 +313,20 @@ "type": "github" } ], - "time": "2024-04-19T03:49:29+00:00" + "time": "2024-06-02T19:54:12+00:00" }, { "name": "amphp/parallel", - "version": "v2.2.9", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/amphp/parallel.git", - "reference": "73d293f1fc4df1bebc3c4fce1432e82dd7032238" + "reference": "9777db1460d1535bc2a843840684fb1205225b87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/parallel/zipball/73d293f1fc4df1bebc3c4fce1432e82dd7032238", - "reference": "73d293f1fc4df1bebc3c4fce1432e82dd7032238", + "url": "https://api.github.com/repos/amphp/parallel/zipball/9777db1460d1535bc2a843840684fb1205225b87", + "reference": "9777db1460d1535bc2a843840684fb1205225b87", "shasum": "" }, "require": { @@ -389,7 +389,7 @@ ], "support": { "issues": "https://github.com/amphp/parallel/issues", - "source": "https://github.com/amphp/parallel/tree/v2.2.9" + "source": "https://github.com/amphp/parallel/tree/v2.3.0" }, "funding": [ { @@ -397,7 +397,7 @@ "type": "github" } ], - "time": "2024-03-24T18:27:44+00:00" + "time": "2024-09-14T19:16:14+00:00" }, { "name": "amphp/parser", @@ -463,16 +463,16 @@ }, { "name": "amphp/pipeline", - "version": "v1.2.0", + "version": "v1.2.1", "source": { "type": "git", "url": "https://github.com/amphp/pipeline.git", - "reference": "f1c2ce35d27ae86ead018adb803eccca7421dd9b" + "reference": "66c095673aa5b6e689e63b52d19e577459129ab3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/pipeline/zipball/f1c2ce35d27ae86ead018adb803eccca7421dd9b", - "reference": "f1c2ce35d27ae86ead018adb803eccca7421dd9b", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/66c095673aa5b6e689e63b52d19e577459129ab3", + "reference": "66c095673aa5b6e689e63b52d19e577459129ab3", "shasum": "" }, "require": { @@ -518,7 +518,7 @@ ], "support": { "issues": "https://github.com/amphp/pipeline/issues", - "source": "https://github.com/amphp/pipeline/tree/v1.2.0" + "source": "https://github.com/amphp/pipeline/tree/v1.2.1" }, "funding": [ { @@ -526,7 +526,7 @@ "type": "github" } ], - "time": "2024-03-10T14:48:16+00:00" + "time": "2024-07-04T00:56:47+00:00" }, { "name": "amphp/process", @@ -740,16 +740,16 @@ }, { "name": "amphp/sync", - "version": "v2.2.0", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/amphp/sync.git", - "reference": "375ef5b54a0d12c38e12728dde05a55e30f2fbec" + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/sync/zipball/375ef5b54a0d12c38e12728dde05a55e30f2fbec", - "reference": "375ef5b54a0d12c38e12728dde05a55e30f2fbec", + "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1", "shasum": "" }, "require": { @@ -803,7 +803,7 @@ ], "support": { "issues": "https://github.com/amphp/sync/issues", - "source": "https://github.com/amphp/sync/tree/v2.2.0" + "source": "https://github.com/amphp/sync/tree/v2.3.0" }, "funding": [ { @@ -811,7 +811,7 @@ "type": "github" } ], - "time": "2024-03-12T01:00:01+00:00" + "time": "2024-08-03T19:31:26+00:00" }, { "name": "amphp/windows-registry", @@ -867,16 +867,16 @@ }, { "name": "aws/aws-crt-php", - "version": "v1.2.5", + "version": "v1.2.6", "source": { "type": "git", "url": "https://github.com/awslabs/aws-crt-php.git", - "reference": "0ea1f04ec5aa9f049f97e012d1ed63b76834a31b" + "reference": "a63485b65b6b3367039306496d49737cf1995408" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/0ea1f04ec5aa9f049f97e012d1ed63b76834a31b", - "reference": "0ea1f04ec5aa9f049f97e012d1ed63b76834a31b", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/a63485b65b6b3367039306496d49737cf1995408", + "reference": "a63485b65b6b3367039306496d49737cf1995408", "shasum": "" }, "require": { @@ -915,22 +915,22 @@ ], "support": { "issues": "https://github.com/awslabs/aws-crt-php/issues", - "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.5" + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.6" }, - "time": "2024-04-19T21:30:56+00:00" + "time": "2024-06-13T17:21:28+00:00" }, { "name": "aws/aws-sdk-php", - "version": "3.308.4", + "version": "3.324.0", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "c88e9df7e076b6e2c652a1c87d2c3af0a9ac30b6" + "reference": "b258712f0d986e00e1143d55246b6f9e344c7184" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c88e9df7e076b6e2c652a1c87d2c3af0a9ac30b6", - "reference": "c88e9df7e076b6e2c652a1c87d2c3af0a9ac30b6", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b258712f0d986e00e1143d55246b6f9e344c7184", + "reference": "b258712f0d986e00e1143d55246b6f9e344c7184", "shasum": "" }, "require": { @@ -983,7 +983,10 @@ ], "psr-4": { "Aws\\": "src/" - } + }, + "exclude-from-classmap": [ + "src/data/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1010,22 +1013,22 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.308.4" + "source": "https://github.com/aws/aws-sdk-php/tree/3.324.0" }, - "time": "2024-05-28T18:05:38+00:00" + "time": "2024-10-10T18:06:36+00:00" }, { "name": "bacon/bacon-qr-code", - "version": "v3.0.0", + "version": "v3.0.1", "source": { "type": "git", "url": "https://github.com/Bacon/BaconQrCode.git", - "reference": "510de6eca6248d77d31b339d62437cc995e2fb41" + "reference": "f9cc1f52b5a463062251d666761178dbdb6b544f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/510de6eca6248d77d31b339d62437cc995e2fb41", - "reference": "510de6eca6248d77d31b339d62437cc995e2fb41", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/f9cc1f52b5a463062251d666761178dbdb6b544f", + "reference": "f9cc1f52b5a463062251d666761178dbdb6b544f", "shasum": "" }, "require": { @@ -1064,9 +1067,9 @@ "homepage": "https://github.com/Bacon/BaconQrCode", "support": { "issues": "https://github.com/Bacon/BaconQrCode/issues", - "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.0" + "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.1" }, - "time": "2024-04-18T11:16:25+00:00" + "time": "2024-10-01T13:55:55+00:00" }, { "name": "brick/math", @@ -1197,72 +1200,6 @@ ], "time": "2023-12-11T17:09:12+00:00" }, - { - "name": "clue/stream-filter", - "version": "v1.7.0", - "source": { - "type": "git", - "url": "https://github.com/clue/stream-filter.git", - "reference": "049509fef80032cb3f051595029ab75b49a3c2f7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/clue/stream-filter/zipball/049509fef80032cb3f051595029ab75b49a3c2f7", - "reference": "049509fef80032cb3f051595029ab75b49a3c2f7", - "shasum": "" - }, - "require": { - "php": ">=5.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "Clue\\StreamFilter\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering" - } - ], - "description": "A simple and modern approach to stream filtering in PHP", - "homepage": "https://github.com/clue/stream-filter", - "keywords": [ - "bucket brigade", - "callback", - "filter", - "php_user_filter", - "stream", - "stream_filter_append", - "stream_filter_register" - ], - "support": { - "issues": "https://github.com/clue/stream-filter/issues", - "source": "https://github.com/clue/stream-filter/tree/v1.7.0" - }, - "funding": [ - { - "url": "https://clue.engineering/support", - "type": "custom" - }, - { - "url": "https://github.com/clue", - "type": "github" - } - ], - "time": "2023-12-20T15:40:13+00:00" - }, { "name": "danharrin/livewire-rate-limiting", "version": "v1.3.1", @@ -1319,23 +1256,23 @@ }, { "name": "dasprid/enum", - "version": "1.0.5", + "version": "1.0.6", "source": { "type": "git", "url": "https://github.com/DASPRiD/Enum.git", - "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016" + "reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/6faf451159fb8ba4126b925ed2d78acfce0dc016", - "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8dfd07c6d2cf31c8da90c53b83c026c7696dda90", + "reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90", "shasum": "" }, "require": { "php": ">=7.1 <9.0" }, "require-dev": { - "phpunit/phpunit": "^7 | ^8 | ^9", + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", "squizlabs/php_codesniffer": "*" }, "type": "library", @@ -1363,9 +1300,9 @@ ], "support": { "issues": "https://github.com/DASPRiD/Enum/issues", - "source": "https://github.com/DASPRiD/Enum/tree/1.0.5" + "source": "https://github.com/DASPRiD/Enum/tree/1.0.6" }, - "time": "2023-08-25T16:18:39+00:00" + "time": "2024-08-09T14:30:48+00:00" }, { "name": "daverandom/libdns", @@ -1413,16 +1350,16 @@ }, { "name": "dflydev/dot-access-data", - "version": "v3.0.2", + "version": "v3.0.3", "source": { "type": "git", "url": "https://github.com/dflydev/dflydev-dot-access-data.git", - "reference": "f41715465d65213d644d3141a6a93081be5d3549" + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/f41715465d65213d644d3141a6a93081be5d3549", - "reference": "f41715465d65213d644d3141a6a93081be5d3549", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", "shasum": "" }, "require": { @@ -1482,9 +1419,9 @@ ], "support": { "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", - "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.2" + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" }, - "time": "2022-10-27T11:44:00+00:00" + "time": "2024-07-08T12:26:09+00:00" }, { "name": "doctrine/cache", @@ -1581,16 +1518,16 @@ }, { "name": "doctrine/dbal", - "version": "3.8.4", + "version": "3.9.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "b05e48a745f722801f55408d0dbd8003b403dbbd" + "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/b05e48a745f722801f55408d0dbd8003b403dbbd", - "reference": "b05e48a745f722801f55408d0dbd8003b403dbbd", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", + "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", "shasum": "" }, "require": { @@ -1606,12 +1543,12 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.10.58", - "phpstan/phpstan-strict-rules": "^1.5", - "phpunit/phpunit": "9.6.16", + "phpstan/phpstan": "1.12.6", + "phpstan/phpstan-strict-rules": "^1.6", + "phpunit/phpunit": "9.6.20", "psalm/plugin-phpunit": "0.18.4", "slevomat/coding-standard": "8.13.1", - "squizlabs/php_codesniffer": "3.9.0", + "squizlabs/php_codesniffer": "3.10.2", "symfony/cache": "^5.4|^6.0|^7.0", "symfony/console": "^4.4|^5.4|^6.0|^7.0", "vimeo/psalm": "4.30.0" @@ -1674,7 +1611,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.8.4" + "source": "https://github.com/doctrine/dbal/tree/3.9.3" }, "funding": [ { @@ -1690,7 +1627,7 @@ "type": "tidelift" } ], - "time": "2024-04-25T07:04:44+00:00" + "time": "2024-10-10T17:56:43+00:00" }, { "name": "doctrine/deprecations", @@ -2000,16 +1937,16 @@ }, { "name": "dragonmantank/cron-expression", - "version": "v3.3.3", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/dragonmantank/cron-expression.git", - "reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a" + "reference": "8c784d071debd117328803d86b2097615b457500" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/adfb1f505deb6384dc8b39804c5065dd3c8c8c0a", - "reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/8c784d071debd117328803d86b2097615b457500", + "reference": "8c784d071debd117328803d86b2097615b457500", "shasum": "" }, "require": { @@ -2022,10 +1959,14 @@ "require-dev": { "phpstan/extension-installer": "^1.0", "phpstan/phpstan": "^1.0", - "phpstan/phpstan-webmozart-assert": "^1.0", "phpunit/phpunit": "^7.0|^8.0|^9.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, "autoload": { "psr-4": { "Cron\\": "src/Cron/" @@ -2049,7 +1990,7 @@ ], "support": { "issues": "https://github.com/dragonmantank/cron-expression/issues", - "source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.3" + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.4.0" }, "funding": [ { @@ -2057,7 +1998,7 @@ "type": "github" } ], - "time": "2023-08-10T19:36:49+00:00" + "time": "2024-10-09T13:47:03+00:00" }, { "name": "egulias/email-validator", @@ -2262,24 +2203,24 @@ }, { "name": "graham-campbell/result-type", - "version": "v1.1.2", + "version": "v1.1.3", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "fbd48bce38f73f8a4ec8583362e732e4095e5862" + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/fbd48bce38f73f8a4ec8583362e732e4095e5862", - "reference": "fbd48bce38f73f8a4ec8583362e732e4095e5862", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.2" + "phpoption/phpoption": "^1.9.3" }, "require-dev": { - "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" }, "type": "library", "autoload": { @@ -2308,7 +2249,7 @@ ], "support": { "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.2" + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" }, "funding": [ { @@ -2320,26 +2261,26 @@ "type": "tidelift" } ], - "time": "2023-11-12T22:16:48+00:00" + "time": "2024-07-20T21:45:45+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.8.1", + "version": "7.9.2", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" + "reference": "d281ed313b989f213357e3be1a179f02196ac99b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", - "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.1", - "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -2350,9 +2291,9 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", - "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "guzzle/client-integration-tests": "3.0.2", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -2430,7 +2371,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.8.1" + "source": "https://github.com/guzzle/guzzle/tree/7.9.2" }, "funding": [ { @@ -2446,20 +2387,20 @@ "type": "tidelift" } ], - "time": "2023-12-03T20:35:24+00:00" + "time": "2024-07-24T11:22:20+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", - "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", + "url": "https://api.github.com/repos/guzzle/promises/zipball/6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", + "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", "shasum": "" }, "require": { @@ -2467,7 +2408,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.36 || ^9.6.15" + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "type": "library", "extra": { @@ -2513,7 +2454,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.2" + "source": "https://github.com/guzzle/promises/tree/2.0.3" }, "funding": [ { @@ -2529,20 +2470,20 @@ "type": "tidelift" } ], - "time": "2023-12-03T20:19:20+00:00" + "time": "2024-07-18T10:29:17+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.6.2", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", - "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", "shasum": "" }, "require": { @@ -2557,8 +2498,8 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.36 || ^9.6.15" + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -2629,7 +2570,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.6.2" + "source": "https://github.com/guzzle/psr7/tree/2.7.0" }, "funding": [ { @@ -2645,7 +2586,7 @@ "type": "tidelift" } ], - "time": "2023-12-03T20:05:35+00:00" + "time": "2024-07-18T11:15:46+00:00" }, { "name": "guzzlehttp/uri-template", @@ -2733,64 +2674,6 @@ ], "time": "2023-12-03T19:50:20+00:00" }, - { - "name": "http-interop/http-factory-guzzle", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/http-interop/http-factory-guzzle.git", - "reference": "8f06e92b95405216b237521cc64c804dd44c4a81" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/http-interop/http-factory-guzzle/zipball/8f06e92b95405216b237521cc64c804dd44c4a81", - "reference": "8f06e92b95405216b237521cc64c804dd44c4a81", - "shasum": "" - }, - "require": { - "guzzlehttp/psr7": "^1.7||^2.0", - "php": ">=7.3", - "psr/http-factory": "^1.0" - }, - "provide": { - "psr/http-factory-implementation": "^1.0" - }, - "require-dev": { - "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^9.5" - }, - "suggest": { - "guzzlehttp/psr7": "Includes an HTTP factory starting in version 2.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Http\\Factory\\Guzzle\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "An HTTP Factory using Guzzle PSR7", - "keywords": [ - "factory", - "http", - "psr-17", - "psr-7" - ], - "support": { - "issues": "https://github.com/http-interop/http-factory-guzzle/issues", - "source": "https://github.com/http-interop/http-factory-guzzle/tree/1.2.0" - }, - "time": "2021-07-21T13:50:14+00:00" - }, { "name": "jean85/pretty-package-versions", "version": "2.0.6", @@ -2910,16 +2793,16 @@ }, { "name": "laravel/fortify", - "version": "v1.21.3", + "version": "v1.24.2", "source": { "type": "git", "url": "https://github.com/laravel/fortify.git", - "reference": "a725684d17959c4750f3b441ff2e94ecde7793a1" + "reference": "42695c45087e5abb3e173725b4f1ef4956a7b47d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/fortify/zipball/a725684d17959c4750f3b441ff2e94ecde7793a1", - "reference": "a725684d17959c4750f3b441ff2e94ecde7793a1", + "url": "https://api.github.com/repos/laravel/fortify/zipball/42695c45087e5abb3e173725b4f1ef4956a7b47d", + "reference": "42695c45087e5abb3e173725b4f1ef4956a7b47d", "shasum": "" }, "require": { @@ -2971,20 +2854,20 @@ "issues": "https://github.com/laravel/fortify/issues", "source": "https://github.com/laravel/fortify" }, - "time": "2024-05-08T18:07:38+00:00" + "time": "2024-09-16T19:20:52+00:00" }, { "name": "laravel/framework", - "version": "v10.48.12", + "version": "v11.27.2", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "590afea38e708022662629fbf5184351fa82cf08" + "reference": "a51d1f2b771c542324a3d9b76a98b1bbc75c0ee9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/590afea38e708022662629fbf5184351fa82cf08", - "reference": "590afea38e708022662629fbf5184351fa82cf08", + "url": "https://api.github.com/repos/laravel/framework/zipball/a51d1f2b771c542324a3d9b76a98b1bbc75c0ee9", + "reference": "a51d1f2b771c542324a3d9b76a98b1bbc75c0ee9", "shasum": "" }, "require": { @@ -3000,44 +2883,44 @@ "ext-openssl": "*", "ext-session": "*", "ext-tokenizer": "*", - "fruitcake/php-cors": "^1.2", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8", "guzzlehttp/uri-template": "^1.0", - "laravel/prompts": "^0.1.9", + "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0", "laravel/serializable-closure": "^1.3", "league/commonmark": "^2.2.1", "league/flysystem": "^3.8.0", "monolog/monolog": "^3.0", - "nesbot/carbon": "^2.67", - "nunomaduro/termwind": "^1.13", - "php": "^8.1", + "nesbot/carbon": "^2.72.2|^3.0", + "nunomaduro/termwind": "^2.0", + "php": "^8.2", "psr/container": "^1.1.1|^2.0.1", "psr/log": "^1.0|^2.0|^3.0", "psr/simple-cache": "^1.0|^2.0|^3.0", "ramsey/uuid": "^4.7", - "symfony/console": "^6.2", - "symfony/error-handler": "^6.2", - "symfony/finder": "^6.2", - "symfony/http-foundation": "^6.4", - "symfony/http-kernel": "^6.2", - "symfony/mailer": "^6.2", - "symfony/mime": "^6.2", - "symfony/process": "^6.2", - "symfony/routing": "^6.2", - "symfony/uid": "^6.2", - "symfony/var-dumper": "^6.2", + "symfony/console": "^7.0", + "symfony/error-handler": "^7.0", + "symfony/finder": "^7.0", + "symfony/http-foundation": "^7.0", + "symfony/http-kernel": "^7.0", + "symfony/mailer": "^7.0", + "symfony/mime": "^7.0", + "symfony/polyfill-php83": "^1.28", + "symfony/process": "^7.0", + "symfony/routing": "^7.0", + "symfony/uid": "^7.0", + "symfony/var-dumper": "^7.0", "tijsverkoyen/css-to-inline-styles": "^2.2.5", "vlucas/phpdotenv": "^5.4.1", "voku/portable-ascii": "^2.0" }, "conflict": { - "carbonphp/carbon-doctrine-types": ">=3.0", - "doctrine/dbal": ">=4.0", "mockery/mockery": "1.6.8", - "phpunit/phpunit": ">=11.0.0", "tightenco/collect": "<5.5.33" }, "provide": { "psr/container-implementation": "1.1|2.0", + "psr/log-implementation": "1.0|2.0|3.0", "psr/simple-cache-implementation": "1.0|2.0|3.0" }, "replace": { @@ -3046,6 +2929,7 @@ "illuminate/bus": "self.version", "illuminate/cache": "self.version", "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", "illuminate/conditionable": "self.version", "illuminate/config": "self.version", "illuminate/console": "self.version", @@ -3073,36 +2957,35 @@ "illuminate/testing": "self.version", "illuminate/translation": "self.version", "illuminate/validation": "self.version", - "illuminate/view": "self.version" + "illuminate/view": "self.version", + "spatie/once": "*" }, "require-dev": { "ably/ably-php": "^1.0", "aws/aws-sdk-php": "^3.235.5", - "doctrine/dbal": "^3.5.1", "ext-gmp": "*", - "fakerphp/faker": "^1.21", - "guzzlehttp/guzzle": "^7.5", + "fakerphp/faker": "^1.23", "league/flysystem-aws-s3-v3": "^3.0", "league/flysystem-ftp": "^3.0", "league/flysystem-path-prefixing": "^3.3", "league/flysystem-read-only": "^3.3", "league/flysystem-sftp-v3": "^3.0", - "mockery/mockery": "^1.5.1", + "mockery/mockery": "^1.6", "nyholm/psr7": "^1.2", - "orchestra/testbench-core": "^8.23.4", - "pda/pheanstalk": "^4.0", - "phpstan/phpstan": "^1.4.7", - "phpunit/phpunit": "^10.0.7", + "orchestra/testbench-core": "^9.5", + "pda/pheanstalk": "^5.0", + "phpstan/phpstan": "^1.11.5", + "phpunit/phpunit": "^10.5|^11.0", "predis/predis": "^2.0.2", - "symfony/cache": "^6.2", - "symfony/http-client": "^6.2.4", - "symfony/psr-http-message-bridge": "^2.0" + "resend/resend-php": "^0.10.0", + "symfony/cache": "^7.0", + "symfony/http-client": "^7.0", + "symfony/psr-http-message-bridge": "^7.0" }, "suggest": { "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.235.5).", - "brianium/paratest": "Required to run tests in parallel (^6.0).", - "doctrine/dbal": "Required to rename columns and drop SQLite columns (^3.5.1).", + "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", "ext-apcu": "Required to use the APC cache driver.", "ext-fileinfo": "Required to use the Filesystem class.", "ext-ftp": "Required to use the Flysystem FTP driver.", @@ -3111,34 +2994,34 @@ "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", "ext-pdo": "Required to use all database features.", "ext-posix": "Required to use all features of the queue worker.", - "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0).", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", "filp/whoops": "Required for friendly error pages in development (^2.14.3).", - "guzzlehttp/guzzle": "Required to use the HTTP Client and the ping methods on schedules (^7.5).", "laravel/tinker": "Required to use the tinker console command (^2.0).", "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.0).", "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.0).", "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.3).", "league/flysystem-read-only": "Required to use read-only disks (^3.3)", "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.0).", - "mockery/mockery": "Required to use mocking (^1.5.1).", + "mockery/mockery": "Required to use mocking (^1.6).", "nyholm/psr7": "Required to use PSR-7 bridging features (^1.2).", - "pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).", - "phpunit/phpunit": "Required to use assertions and run tests (^9.5.8|^10.0.7).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5|^11.0).", "predis/predis": "Required to use the predis connector (^2.0.2).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", - "symfony/cache": "Required to PSR-6 cache bridge (^6.2).", - "symfony/filesystem": "Required to enable support for relative symbolic links (^6.2).", - "symfony/http-client": "Required to enable support for the Symfony API mail transports (^6.2).", - "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^6.2).", - "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^6.2).", - "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^2.0)." + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.0).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.0).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.0).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.0).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.0)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "10.x-dev" + "dev-master": "11.x-dev" } }, "autoload": { @@ -3147,6 +3030,8 @@ "src/Illuminate/Events/functions.php", "src/Illuminate/Filesystem/functions.php", "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Support/functions.php", "src/Illuminate/Support/helpers.php" ], "psr-4": { @@ -3178,20 +3063,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-05-28T15:46:19+00:00" + "time": "2024-10-09T04:17:35+00:00" }, { "name": "laravel/horizon", - "version": "v5.24.4", + "version": "v5.29.1", "source": { "type": "git", "url": "https://github.com/laravel/horizon.git", - "reference": "8d31ff178bf5493efc2b2629c10612054f31f584" + "reference": "9f482f21c23ed01c2366d1157843165165579c23" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/horizon/zipball/8d31ff178bf5493efc2b2629c10612054f31f584", - "reference": "8d31ff178bf5493efc2b2629c10612054f31f584", + "url": "https://api.github.com/repos/laravel/horizon/zipball/9f482f21c23ed01c2366d1157843165165579c23", + "reference": "9f482f21c23ed01c2366d1157843165165579c23", "shasum": "" }, "require": { @@ -3255,22 +3140,99 @@ ], "support": { "issues": "https://github.com/laravel/horizon/issues", - "source": "https://github.com/laravel/horizon/tree/v5.24.4" + "source": "https://github.com/laravel/horizon/tree/v5.29.1" }, - "time": "2024-05-03T13:34:14+00:00" + "time": "2024-10-08T18:23:02+00:00" }, { - "name": "laravel/prompts", - "version": "v0.1.23", + "name": "laravel/pail", + "version": "v1.1.5", "source": { "type": "git", - "url": "https://github.com/laravel/prompts.git", - "reference": "9bc4df7c699b0452c6b815e64a2d84b6d7f99400" + "url": "https://github.com/laravel/pail.git", + "reference": "b33ad8321416fe86efed7bf398f3306c47b4871b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/9bc4df7c699b0452c6b815e64a2d84b6d7f99400", - "reference": "9bc4df7c699b0452c6b815e64a2d84b6d7f99400", + "url": "https://api.github.com/repos/laravel/pail/zipball/b33ad8321416fe86efed7bf398f3306c47b4871b", + "reference": "b33ad8321416fe86efed7bf398f3306c47b4871b", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/console": "^10.24|^11.0", + "illuminate/contracts": "^10.24|^11.0", + "illuminate/log": "^10.24|^11.0", + "illuminate/process": "^10.24|^11.0", + "illuminate/support": "^10.24|^11.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0" + }, + "require-dev": { + "laravel/pint": "^1.13", + "orchestra/testbench": "^8.12|^9.0", + "pestphp/pest": "^2.20", + "pestphp/pest-plugin-type-coverage": "^2.3", + "phpstan/phpstan": "^1.10", + "symfony/var-dumper": "^6.3|^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2024-10-15T20:06:24+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.1.25", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "7b4029a84c37cb2725fc7f011586e2997040bc95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/7b4029a84c37cb2725fc7f011586e2997040bc95", + "reference": "7b4029a84c37cb2725fc7f011586e2997040bc95", "shasum": "" }, "require": { @@ -3313,43 +3275,41 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.1.23" + "source": "https://github.com/laravel/prompts/tree/v0.1.25" }, - "time": "2024-05-27T13:53:20+00:00" + "time": "2024-08-12T22:06:33+00:00" }, { "name": "laravel/sanctum", - "version": "v3.3.3", + "version": "v4.0.3", "source": { "type": "git", "url": "https://github.com/laravel/sanctum.git", - "reference": "8c104366459739f3ada0e994bcd3e6fd681ce3d5" + "reference": "54aea9d13743ae8a6cdd3c28dbef128a17adecab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sanctum/zipball/8c104366459739f3ada0e994bcd3e6fd681ce3d5", - "reference": "8c104366459739f3ada0e994bcd3e6fd681ce3d5", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/54aea9d13743ae8a6cdd3c28dbef128a17adecab", + "reference": "54aea9d13743ae8a6cdd3c28dbef128a17adecab", "shasum": "" }, "require": { "ext-json": "*", - "illuminate/console": "^9.21|^10.0", - "illuminate/contracts": "^9.21|^10.0", - "illuminate/database": "^9.21|^10.0", - "illuminate/support": "^9.21|^10.0", - "php": "^8.0.2" + "illuminate/console": "^11.0", + "illuminate/contracts": "^11.0", + "illuminate/database": "^11.0", + "illuminate/support": "^11.0", + "php": "^8.2", + "symfony/console": "^7.0" }, "require-dev": { - "mockery/mockery": "^1.0", - "orchestra/testbench": "^7.28.2|^8.8.3", + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.0", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.6" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - }, "laravel": { "providers": [ "Laravel\\Sanctum\\SanctumServiceProvider" @@ -3381,30 +3341,31 @@ "issues": "https://github.com/laravel/sanctum/issues", "source": "https://github.com/laravel/sanctum" }, - "time": "2023-12-19T18:44:48+00:00" + "time": "2024-09-27T14:55:41+00:00" }, { "name": "laravel/serializable-closure", - "version": "v1.3.3", + "version": "v1.3.5", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "3dbf8a8e914634c48d389c1234552666b3d43754" + "reference": "1dc4a3dbfa2b7628a3114e43e32120cce7cdda9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/3dbf8a8e914634c48d389c1234552666b3d43754", - "reference": "3dbf8a8e914634c48d389c1234552666b3d43754", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/1dc4a3dbfa2b7628a3114e43e32120cce7cdda9c", + "reference": "1dc4a3dbfa2b7628a3114e43e32120cce7cdda9c", "shasum": "" }, "require": { "php": "^7.3|^8.0" }, "require-dev": { - "nesbot/carbon": "^2.61", + "illuminate/support": "^8.0|^9.0|^10.0|^11.0", + "nesbot/carbon": "^2.61|^3.0", "pestphp/pest": "^1.21.3", "phpstan/phpstan": "^1.8.2", - "symfony/var-dumper": "^5.4.11" + "symfony/var-dumper": "^5.4.11|^6.2.0|^7.0.0" }, "type": "library", "extra": { @@ -3441,20 +3402,20 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2023-11-08T14:08:06+00:00" + "time": "2024-09-23T13:33:08+00:00" }, { "name": "laravel/socialite", - "version": "v5.14.0", + "version": "v5.16.0", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "c7b0193a3753a29aff8ce80aa2f511917e6ed68a" + "reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/c7b0193a3753a29aff8ce80aa2f511917e6ed68a", - "reference": "c7b0193a3753a29aff8ce80aa2f511917e6ed68a", + "url": "https://api.github.com/repos/laravel/socialite/zipball/40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf", + "reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf", "shasum": "" }, "require": { @@ -3513,20 +3474,20 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2024-05-03T20:31:38+00:00" + "time": "2024-09-03T09:46:57+00:00" }, { "name": "laravel/tinker", - "version": "v2.9.0", + "version": "v2.10.0", "source": { "type": "git", "url": "https://github.com/laravel/tinker.git", - "reference": "502e0fe3f0415d06d5db1f83a472f0f3b754bafe" + "reference": "ba4d51eb56de7711b3a37d63aa0643e99a339ae5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/502e0fe3f0415d06d5db1f83a472f0f3b754bafe", - "reference": "502e0fe3f0415d06d5db1f83a472f0f3b754bafe", + "url": "https://api.github.com/repos/laravel/tinker/zipball/ba4d51eb56de7711b3a37d63aa0643e99a339ae5", + "reference": "ba4d51eb56de7711b3a37d63aa0643e99a339ae5", "shasum": "" }, "require": { @@ -3577,9 +3538,9 @@ ], "support": { "issues": "https://github.com/laravel/tinker/issues", - "source": "https://github.com/laravel/tinker/tree/v2.9.0" + "source": "https://github.com/laravel/tinker/tree/v2.10.0" }, - "time": "2024-01-04T16:10:04+00:00" + "time": "2024-09-23T13:32:56+00:00" }, { "name": "laravel/ui", @@ -3646,38 +3607,38 @@ }, { "name": "lcobucci/jwt", - "version": "5.3.0", + "version": "5.4.0", "source": { "type": "git", "url": "https://github.com/lcobucci/jwt.git", - "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83" + "reference": "aac4fd512681fd5cb4b77d2105ab7ec700c72051" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/08071d8d2c7f4b00222cc4b1fb6aa46990a80f83", - "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/aac4fd512681fd5cb4b77d2105ab7ec700c72051", + "reference": "aac4fd512681fd5cb4b77d2105ab7ec700c72051", "shasum": "" }, "require": { "ext-openssl": "*", "ext-sodium": "*", - "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", "psr/clock": "^1.0" }, "require-dev": { - "infection/infection": "^0.27.0", - "lcobucci/clock": "^3.0", + "infection/infection": "^0.29", + "lcobucci/clock": "^3.2", "lcobucci/coding-standard": "^11.0", - "phpbench/phpbench": "^1.2.9", + "phpbench/phpbench": "^1.2", "phpstan/extension-installer": "^1.2", "phpstan/phpstan": "^1.10.7", "phpstan/phpstan-deprecation-rules": "^1.1.3", "phpstan/phpstan-phpunit": "^1.3.10", "phpstan/phpstan-strict-rules": "^1.5.0", - "phpunit/phpunit": "^10.2.6" + "phpunit/phpunit": "^11.1" }, "suggest": { - "lcobucci/clock": ">= 3.0" + "lcobucci/clock": ">= 3.2" }, "type": "library", "autoload": { @@ -3703,7 +3664,7 @@ ], "support": { "issues": "https://github.com/lcobucci/jwt/issues", - "source": "https://github.com/lcobucci/jwt/tree/5.3.0" + "source": "https://github.com/lcobucci/jwt/tree/5.4.0" }, "funding": [ { @@ -3715,20 +3676,20 @@ "type": "patreon" } ], - "time": "2024-04-11T23:07:54+00:00" + "time": "2024-10-08T22:06:45+00:00" }, { "name": "league/commonmark", - "version": "2.4.2", + "version": "2.5.3", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf" + "reference": "b650144166dfa7703e62a22e493b853b58d874b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/91c24291965bd6d7c46c46a12ba7492f83b1cadf", - "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/b650144166dfa7703e62a22e493b853b58d874b0", + "reference": "b650144166dfa7703e62a22e493b853b58d874b0", "shasum": "" }, "require": { @@ -3741,8 +3702,8 @@ }, "require-dev": { "cebe/markdown": "^1.0", - "commonmark/cmark": "0.30.3", - "commonmark/commonmark.js": "0.30.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", "composer/package-versions-deprecated": "^1.8", "embed/embed": "^4.4", "erusev/parsedown": "^1.0", @@ -3764,7 +3725,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.5-dev" + "dev-main": "2.6-dev" } }, "autoload": { @@ -3821,7 +3782,7 @@ "type": "tidelift" } ], - "time": "2024-02-02T11:59:32+00:00" + "time": "2024-08-16T11:46:16+00:00" }, { "name": "league/config", @@ -3907,16 +3868,16 @@ }, { "name": "league/flysystem", - "version": "3.28.0", + "version": "3.29.1", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "e611adab2b1ae2e3072fa72d62c62f52c2bf1f0c" + "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/e611adab2b1ae2e3072fa72d62c62f52c2bf1f0c", - "reference": "e611adab2b1ae2e3072fa72d62c62f52c2bf1f0c", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/edc1bb7c86fab0776c3287dbd19b5fa278347319", + "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319", "shasum": "" }, "require": { @@ -3984,22 +3945,22 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.28.0" + "source": "https://github.com/thephpleague/flysystem/tree/3.29.1" }, - "time": "2024-05-22T10:09:12+00:00" + "time": "2024-10-08T08:58:34+00:00" }, { "name": "league/flysystem-aws-s3-v3", - "version": "3.28.0", + "version": "3.29.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", - "reference": "22071ef1604bc776f5ff2468ac27a752514665c8" + "reference": "c6ff6d4606e48249b63f269eba7fabdb584e76a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/22071ef1604bc776f5ff2468ac27a752514665c8", - "reference": "22071ef1604bc776f5ff2468ac27a752514665c8", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/c6ff6d4606e48249b63f269eba7fabdb584e76a9", + "reference": "c6ff6d4606e48249b63f269eba7fabdb584e76a9", "shasum": "" }, "require": { @@ -4039,22 +4000,22 @@ "storage" ], "support": { - "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.28.0" + "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.29.0" }, - "time": "2024-05-06T20:05:52+00:00" + "time": "2024-08-17T13:10:48+00:00" }, { "name": "league/flysystem-local", - "version": "3.28.0", + "version": "3.29.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "13f22ea8be526ea58c2ddff9e158ef7c296e4f40" + "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/13f22ea8be526ea58c2ddff9e158ef7c296e4f40", - "reference": "13f22ea8be526ea58c2ddff9e158ef7c296e4f40", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/e0e8d52ce4b2ed154148453d321e97c8e931bd27", + "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27", "shasum": "" }, "require": { @@ -4088,22 +4049,22 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.28.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.29.0" }, - "time": "2024-05-06T20:05:52+00:00" + "time": "2024-08-09T21:24:39+00:00" }, { "name": "league/flysystem-sftp-v3", - "version": "3.28.0", + "version": "3.29.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-sftp-v3.git", - "reference": "abedadd3c64d4f0e276d6ecc796ec8194d136b41" + "reference": "ce9b209e2fbe33122c755ffc18eb4d5bd256f252" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-sftp-v3/zipball/abedadd3c64d4f0e276d6ecc796ec8194d136b41", - "reference": "abedadd3c64d4f0e276d6ecc796ec8194d136b41", + "url": "https://api.github.com/repos/thephpleague/flysystem-sftp-v3/zipball/ce9b209e2fbe33122c755ffc18eb4d5bd256f252", + "reference": "ce9b209e2fbe33122c755ffc18eb4d5bd256f252", "shasum": "" }, "require": { @@ -4137,22 +4098,22 @@ "sftp" ], "support": { - "source": "https://github.com/thephpleague/flysystem-sftp-v3/tree/3.28.0" + "source": "https://github.com/thephpleague/flysystem-sftp-v3/tree/3.29.0" }, - "time": "2024-05-06T20:05:52+00:00" + "time": "2024-08-14T19:35:54+00:00" }, { "name": "league/mime-type-detection", - "version": "1.15.0", + "version": "1.16.0", "source": { "type": "git", "url": "https://github.com/thephpleague/mime-type-detection.git", - "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301" + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301", - "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", "shasum": "" }, "require": { @@ -4183,7 +4144,7 @@ "description": "Mime-type detection for Flysystem", "support": { "issues": "https://github.com/thephpleague/mime-type-detection/issues", - "source": "https://github.com/thephpleague/mime-type-detection/tree/1.15.0" + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" }, "funding": [ { @@ -4195,7 +4156,7 @@ "type": "tidelift" } ], - "time": "2024-01-28T23:22:08+00:00" + "time": "2024-09-21T08:32:55+00:00" }, { "name": "league/oauth1-client", @@ -4523,17 +4484,79 @@ "time": "2024-03-14T14:03:32+00:00" }, { - "name": "lorisleiva/laravel-actions", - "version": "v2.8.0", + "name": "log1x/laravel-webfonts", + "version": "v1.0.1", "source": { "type": "git", - "url": "https://github.com/lorisleiva/laravel-actions.git", - "reference": "d5c2ca544f40d85f877b38eb6d23e9c967ecb69f" + "url": "https://github.com/Log1x/laravel-webfonts.git", + "reference": "0d38122aa7f5501394006a6715f7d97dac223507" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lorisleiva/laravel-actions/zipball/d5c2ca544f40d85f877b38eb6d23e9c967ecb69f", - "reference": "d5c2ca544f40d85f877b38eb6d23e9c967ecb69f", + "url": "https://api.github.com/repos/Log1x/laravel-webfonts/zipball/0d38122aa7f5501394006a6715f7d97dac223507", + "reference": "0d38122aa7f5501394006a6715f7d97dac223507", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.8", + "laravel/prompts": "^0.1.15", + "php": ">=8.1" + }, + "require-dev": { + "illuminate/console": "^10.41", + "illuminate/http": "^10.41", + "illuminate/support": "^10.41", + "laravel/pint": "^1.13" + }, + "type": "package", + "extra": { + "laravel": { + "providers": [ + "Log1x\\LaravelWebfonts\\WebfontsServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Log1x\\LaravelWebfonts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brandon Nifong", + "email": "brandon@tendency.me", + "homepage": "https://github.com/log1x" + } + ], + "description": "Download, install, and preload over 1500 Google fonts locally in your Laravel project", + "support": { + "issues": "https://github.com/Log1x/laravel-webfonts/issues", + "source": "https://github.com/Log1x/laravel-webfonts/tree/v1.0.1" + }, + "funding": [ + { + "url": "https://github.com/Log1x", + "type": "github" + } + ], + "time": "2024-03-28T11:53:11+00:00" + }, + { + "name": "lorisleiva/laravel-actions", + "version": "v2.8.4", + "source": { + "type": "git", + "url": "https://github.com/lorisleiva/laravel-actions.git", + "reference": "5a168bfdd3b75dd6ff259019d4aeef784bbd5403" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lorisleiva/laravel-actions/zipball/5a168bfdd3b75dd6ff259019d4aeef784bbd5403", + "reference": "5a168bfdd3b75dd6ff259019d4aeef784bbd5403", "shasum": "" }, "require": { @@ -4542,7 +4565,7 @@ "php": "^8.1" }, "require-dev": { - "orchestra/testbench": "^9.0", + "orchestra/testbench": "^8.0|^9.0", "pestphp/pest": "^1.23|^2.34", "phpunit/phpunit": "^9.6|^10.0" }, @@ -4583,11 +4606,12 @@ "controller", "job", "laravel", + "listener", "object" ], "support": { "issues": "https://github.com/lorisleiva/laravel-actions/issues", - "source": "https://github.com/lorisleiva/laravel-actions/tree/v2.8.0" + "source": "https://github.com/lorisleiva/laravel-actions/tree/v2.8.4" }, "funding": [ { @@ -4595,7 +4619,7 @@ "type": "github" } ], - "time": "2024-03-13T12:47:32+00:00" + "time": "2024-09-10T09:57:29+00:00" }, { "name": "lorisleiva/lody", @@ -4671,16 +4695,16 @@ }, { "name": "monolog/monolog", - "version": "3.6.0", + "version": "3.7.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "4b18b21a5527a3d5ffdac2fd35d3ab25a9597654" + "reference": "f4393b648b78a5408747de94fca38beb5f7e9ef8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/4b18b21a5527a3d5ffdac2fd35d3ab25a9597654", - "reference": "4b18b21a5527a3d5ffdac2fd35d3ab25a9597654", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f4393b648b78a5408747de94fca38beb5f7e9ef8", + "reference": "f4393b648b78a5408747de94fca38beb5f7e9ef8", "shasum": "" }, "require": { @@ -4756,7 +4780,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.6.0" + "source": "https://github.com/Seldaek/monolog/tree/3.7.0" }, "funding": [ { @@ -4768,20 +4792,20 @@ "type": "tidelift" } ], - "time": "2024-04-12T21:02:21+00:00" + "time": "2024-06-28T09:40:51+00:00" }, { "name": "mtdowling/jmespath.php", - "version": "2.7.0", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/jmespath/jmespath.php.git", - "reference": "bbb69a935c2cbb0c03d7f481a238027430f6440b" + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/bbb69a935c2cbb0c03d7f481a238027430f6440b", - "reference": "bbb69a935c2cbb0c03d7f481a238027430f6440b", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc", "shasum": "" }, "require": { @@ -4798,7 +4822,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev" + "dev-master": "2.8-dev" } }, "autoload": { @@ -4832,48 +4856,47 @@ ], "support": { "issues": "https://github.com/jmespath/jmespath.php/issues", - "source": "https://github.com/jmespath/jmespath.php/tree/2.7.0" + "source": "https://github.com/jmespath/jmespath.php/tree/2.8.0" }, - "time": "2023-08-25T10:54:48+00:00" + "time": "2024-09-04T18:46:31+00:00" }, { "name": "nesbot/carbon", - "version": "2.72.3", + "version": "3.8.0", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "0c6fd108360c562f6e4fd1dedb8233b423e91c83" + "reference": "bbd3eef89af8ba66a3aa7952b5439168fbcc529f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/0c6fd108360c562f6e4fd1dedb8233b423e91c83", - "reference": "0c6fd108360c562f6e4fd1dedb8233b423e91c83", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/bbd3eef89af8ba66a3aa7952b5439168fbcc529f", + "reference": "bbd3eef89af8ba66a3aa7952b5439168fbcc529f", "shasum": "" }, "require": { "carbonphp/carbon-doctrine-types": "*", "ext-json": "*", - "php": "^7.1.8 || ^8.0", + "php": "^8.1", "psr/clock": "^1.0", + "symfony/clock": "^6.3 || ^7.0", "symfony/polyfill-mbstring": "^1.0", - "symfony/polyfill-php80": "^1.16", - "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0" + "symfony/translation": "^4.4.18 || ^5.2.1|| ^6.0 || ^7.0" }, "provide": { "psr/clock-implementation": "1.0" }, "require-dev": { - "doctrine/dbal": "^2.0 || ^3.1.4 || ^4.0", - "doctrine/orm": "^2.7 || ^3.0", - "friendsofphp/php-cs-fixer": "^3.0", - "kylekatarnls/multi-tester": "^2.0", - "ondrejmirtes/better-reflection": "*", - "phpmd/phpmd": "^2.9", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.12.99 || ^1.7.14", - "phpunit/php-file-iterator": "^2.0.5 || ^3.0.6", - "phpunit/phpunit": "^7.5.20 || ^8.5.26 || ^9.5.20", - "squizlabs/php_codesniffer": "^3.4" + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.57.2", + "kylekatarnls/multi-tester": "^2.5.3", + "ondrejmirtes/better-reflection": "^6.25.0.4", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^1.11.2", + "phpunit/phpunit": "^10.5.20", + "squizlabs/php_codesniffer": "^3.9.0" }, "bin": [ "bin/carbon" @@ -4881,8 +4904,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-3.x": "3.x-dev", - "dev-master": "2.x-dev" + "dev-master": "3.x-dev", + "dev-2.x": "2.x-dev" }, "laravel": { "providers": [ @@ -4941,28 +4964,28 @@ "type": "tidelift" } ], - "time": "2024-01-25T10:35:09+00:00" + "time": "2024-08-19T06:22:39+00:00" }, { "name": "nette/schema", - "version": "v1.3.0", + "version": "v1.3.2", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188" + "reference": "da801d52f0354f70a638673c4a0f04e16529431d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/a6d3a6d1f545f01ef38e60f375d1cf1f4de98188", - "reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188", + "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", + "reference": "da801d52f0354f70a638673c4a0f04e16529431d", "shasum": "" }, "require": { "nette/utils": "^4.0", - "php": "8.1 - 8.3" + "php": "8.1 - 8.4" }, "require-dev": { - "nette/tester": "^2.4", + "nette/tester": "^2.5.2", "phpstan/phpstan-nette": "^1.0", "tracy/tracy": "^2.8" }, @@ -5001,26 +5024,26 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.3.0" + "source": "https://github.com/nette/schema/tree/v1.3.2" }, - "time": "2023-12-11T11:54:22+00:00" + "time": "2024-10-06T23:10:23+00:00" }, { "name": "nette/utils", - "version": "v4.0.4", + "version": "v4.0.5", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "d3ad0aa3b9f934602cb3e3902ebccf10be34d218" + "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/d3ad0aa3b9f934602cb3e3902ebccf10be34d218", - "reference": "d3ad0aa3b9f934602cb3e3902ebccf10be34d218", + "url": "https://api.github.com/repos/nette/utils/zipball/736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", + "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", "shasum": "" }, "require": { - "php": ">=8.0 <8.4" + "php": "8.0 - 8.4" }, "conflict": { "nette/finder": "<3", @@ -5087,22 +5110,22 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.4" + "source": "https://github.com/nette/utils/tree/v4.0.5" }, - "time": "2024-01-17T16:50:36+00:00" + "time": "2024-08-07T15:39:19+00:00" }, { "name": "nikic/php-parser", - "version": "v5.0.2", + "version": "v5.3.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", - "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", "shasum": "" }, "require": { @@ -5113,7 +5136,7 @@ }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -5145,9 +5168,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" }, - "time": "2024-03-05T20:51:40+00:00" + "time": "2024-10-08T18:51:32+00:00" }, { "name": "nubs/random-name-generator", @@ -5204,33 +5227,32 @@ }, { "name": "nunomaduro/termwind", - "version": "v1.15.1", + "version": "v2.1.0", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "8ab0b32c8caa4a2e09700ea32925441385e4a5dc" + "reference": "e5f21eade88689536c0cdad4c3cd75f3ed26e01a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/8ab0b32c8caa4a2e09700ea32925441385e4a5dc", - "reference": "8ab0b32c8caa4a2e09700ea32925441385e4a5dc", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/e5f21eade88689536c0cdad4c3cd75f3ed26e01a", + "reference": "e5f21eade88689536c0cdad4c3cd75f3ed26e01a", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": "^8.0", - "symfony/console": "^5.3.0|^6.0.0" + "php": "^8.2", + "symfony/console": "^7.0.4" }, "require-dev": { - "ergebnis/phpstan-rules": "^1.0.", - "illuminate/console": "^8.0|^9.0", - "illuminate/support": "^8.0|^9.0", - "laravel/pint": "^1.0.0", - "pestphp/pest": "^1.21.0", - "pestphp/pest-plugin-mock": "^1.0", - "phpstan/phpstan": "^1.4.6", - "phpstan/phpstan-strict-rules": "^1.1.0", - "symfony/var-dumper": "^5.2.7|^6.0.0", + "ergebnis/phpstan-rules": "^2.2.0", + "illuminate/console": "^11.1.1", + "laravel/pint": "^1.15.0", + "mockery/mockery": "^1.6.11", + "pestphp/pest": "^2.34.6", + "phpstan/phpstan": "^1.10.66", + "phpstan/phpstan-strict-rules": "^1.5.2", + "symfony/var-dumper": "^7.0.4", "thecodingmachine/phpstan-strict-rules": "^1.0.0" }, "type": "library", @@ -5239,6 +5261,9 @@ "providers": [ "Termwind\\Laravel\\TermwindServiceProvider" ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" } }, "autoload": { @@ -5270,7 +5295,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/termwind/issues", - "source": "https://github.com/nunomaduro/termwind/tree/v1.15.1" + "source": "https://github.com/nunomaduro/termwind/tree/v2.1.0" }, "funding": [ { @@ -5286,20 +5311,20 @@ "type": "github" } ], - "time": "2023-02-08T01:06:31+00:00" + "time": "2024-09-05T15:25:50+00:00" }, { "name": "nyholm/psr7", - "version": "1.8.1", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/Nyholm/psr7.git", - "reference": "aa5fc277a4f5508013d571341ade0c3886d4d00e" + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Nyholm/psr7/zipball/aa5fc277a4f5508013d571341ade0c3886d4d00e", - "reference": "aa5fc277a4f5508013d571341ade0c3886d4d00e", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", "shasum": "" }, "require": { @@ -5352,7 +5377,7 @@ ], "support": { "issues": "https://github.com/Nyholm/psr7/issues", - "source": "https://github.com/Nyholm/psr7/tree/1.8.1" + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" }, "funding": [ { @@ -5364,28 +5389,28 @@ "type": "github" } ], - "time": "2023-11-13T09:31:12+00:00" + "time": "2024-09-09T07:06:30+00:00" }, { "name": "paragonie/constant_time_encoding", - "version": "v2.7.0", + "version": "v3.0.0", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105" + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/52a0d99e69f56b9ec27ace92ba56897fe6993105", - "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", "shasum": "" }, "require": { - "php": "^7|^8" + "php": "^8" }, "require-dev": { - "phpunit/phpunit": "^6|^7|^8|^9", - "vimeo/psalm": "^1|^2|^3|^4" + "phpunit/phpunit": "^9", + "vimeo/psalm": "^4|^5" }, "type": "library", "autoload": { @@ -5431,7 +5456,7 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2024-05-08T12:18:48+00:00" + "time": "2024-05-08T12:36:18+00:00" }, { "name": "paragonie/random_compat", @@ -5570,385 +5595,132 @@ "time": "2024-04-22T22:05:04+00:00" }, { - "name": "php-http/client-common", - "version": "2.7.1", + "name": "php-di/invoker", + "version": "2.3.4", "source": { "type": "git", - "url": "https://github.com/php-http/client-common.git", - "reference": "1e19c059b0e4d5f717bf5d524d616165aeab0612" + "url": "https://github.com/PHP-DI/Invoker.git", + "reference": "33234b32dafa8eb69202f950a1fc92055ed76a86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/client-common/zipball/1e19c059b0e4d5f717bf5d524d616165aeab0612", - "reference": "1e19c059b0e4d5f717bf5d524d616165aeab0612", + "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/33234b32dafa8eb69202f950a1fc92055ed76a86", + "reference": "33234b32dafa8eb69202f950a1fc92055ed76a86", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0", - "php-http/httplug": "^2.0", - "php-http/message": "^1.6", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.0 || ^2.0", - "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0", - "symfony/polyfill-php80": "^1.17" + "php": ">=7.3", + "psr/container": "^1.0|^2.0" }, "require-dev": { - "doctrine/instantiator": "^1.1", - "guzzlehttp/psr7": "^1.4", - "nyholm/psr7": "^1.2", - "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", - "phpspec/prophecy": "^1.10.2", - "phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7" - }, - "suggest": { - "ext-json": "To detect JSON responses with the ContentTypePlugin", - "ext-libxml": "To detect XML responses with the ContentTypePlugin", - "php-http/cache-plugin": "PSR-6 Cache plugin", - "php-http/logger-plugin": "PSR-3 Logger plugin", - "php-http/stopwatch-plugin": "Symfony Stopwatch plugin" + "athletic/athletic": "~0.1.8", + "mnapoli/hard-mode": "~0.3.0", + "phpunit/phpunit": "^9.0" }, "type": "library", "autoload": { "psr-4": { - "Http\\Client\\Common\\": "src/" + "Invoker\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" - } - ], - "description": "Common HTTP Client implementations and tools for HTTPlug", - "homepage": "http://httplug.io", + "description": "Generic and extensible callable invoker", + "homepage": "https://github.com/PHP-DI/Invoker", "keywords": [ - "client", - "common", - "http", - "httplug" + "callable", + "dependency", + "dependency-injection", + "injection", + "invoke", + "invoker" ], "support": { - "issues": "https://github.com/php-http/client-common/issues", - "source": "https://github.com/php-http/client-common/tree/2.7.1" + "issues": "https://github.com/PHP-DI/Invoker/issues", + "source": "https://github.com/PHP-DI/Invoker/tree/2.3.4" }, - "time": "2023-11-30T10:31:25+00:00" + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2023-09-08T09:24:21+00:00" }, { - "name": "php-http/discovery", - "version": "1.19.4", + "name": "php-di/php-di", + "version": "7.0.7", "source": { "type": "git", - "url": "https://github.com/php-http/discovery.git", - "reference": "0700efda8d7526335132360167315fdab3aeb599" + "url": "https://github.com/PHP-DI/PHP-DI.git", + "reference": "e87435e3c0e8f22977adc5af0d5cdcc467e15cf1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/discovery/zipball/0700efda8d7526335132360167315fdab3aeb599", - "reference": "0700efda8d7526335132360167315fdab3aeb599", + "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/e87435e3c0e8f22977adc5af0d5cdcc467e15cf1", + "reference": "e87435e3c0e8f22977adc5af0d5cdcc467e15cf1", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0|^2.0", - "php": "^7.1 || ^8.0" - }, - "conflict": { - "nyholm/psr7": "<1.0", - "zendframework/zend-diactoros": "*" + "laravel/serializable-closure": "^1.0", + "php": ">=8.0", + "php-di/invoker": "^2.0", + "psr/container": "^1.1 || ^2.0" }, "provide": { - "php-http/async-client-implementation": "*", - "php-http/client-implementation": "*", - "psr/http-client-implementation": "*", - "psr/http-factory-implementation": "*", - "psr/http-message-implementation": "*" + "psr/container-implementation": "^1.0" }, "require-dev": { - "composer/composer": "^1.0.2|^2.0", - "graham-campbell/phpspec-skip-example-extension": "^5.0", - "php-http/httplug": "^1.0 || ^2.0", - "php-http/message-factory": "^1.0", - "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", - "sebastian/comparator": "^3.0.5 || ^4.0.8", - "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" - }, - "type": "composer-plugin", - "extra": { - "class": "Http\\Discovery\\Composer\\Plugin", - "plugin-optional": true - }, - "autoload": { - "psr-4": { - "Http\\Discovery\\": "src/" - }, - "exclude-from-classmap": [ - "src/Composer/Plugin.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" - } - ], - "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", - "homepage": "http://php-http.org", - "keywords": [ - "adapter", - "client", - "discovery", - "factory", - "http", - "message", - "psr17", - "psr7" - ], - "support": { - "issues": "https://github.com/php-http/discovery/issues", - "source": "https://github.com/php-http/discovery/tree/1.19.4" - }, - "time": "2024-03-29T13:00:05+00:00" - }, - { - "name": "php-http/httplug", - "version": "2.4.0", - "source": { - "type": "git", - "url": "https://github.com/php-http/httplug.git", - "reference": "625ad742c360c8ac580fcc647a1541d29e257f67" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-http/httplug/zipball/625ad742c360c8ac580fcc647a1541d29e257f67", - "reference": "625ad742c360c8ac580fcc647a1541d29e257f67", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0", - "php-http/promise": "^1.1", - "psr/http-client": "^1.0", - "psr/http-message": "^1.0 || ^2.0" - }, - "require-dev": { - "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0", - "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Http\\Client\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Eric GELOEN", - "email": "geloen.eric@gmail.com" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://sagikazarmark.hu" - } - ], - "description": "HTTPlug, the HTTP client abstraction for PHP", - "homepage": "http://httplug.io", - "keywords": [ - "client", - "http" - ], - "support": { - "issues": "https://github.com/php-http/httplug/issues", - "source": "https://github.com/php-http/httplug/tree/2.4.0" - }, - "time": "2023-04-14T15:10:03+00:00" - }, - { - "name": "php-http/message", - "version": "1.16.1", - "source": { - "type": "git", - "url": "https://github.com/php-http/message.git", - "reference": "5997f3289332c699fa2545c427826272498a2088" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-http/message/zipball/5997f3289332c699fa2545c427826272498a2088", - "reference": "5997f3289332c699fa2545c427826272498a2088", - "shasum": "" - }, - "require": { - "clue/stream-filter": "^1.5", - "php": "^7.2 || ^8.0", - "psr/http-message": "^1.1 || ^2.0" - }, - "provide": { - "php-http/message-factory-implementation": "1.0" - }, - "require-dev": { - "ergebnis/composer-normalize": "^2.6", - "ext-zlib": "*", - "guzzlehttp/psr7": "^1.0 || ^2.0", - "laminas/laminas-diactoros": "^2.0 || ^3.0", - "php-http/message-factory": "^1.0.2", - "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", - "slim/slim": "^3.0" + "friendsofphp/php-cs-fixer": "^3", + "friendsofphp/proxy-manager-lts": "^1", + "mnapoli/phpunit-easymock": "^1.3", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4.6" }, "suggest": { - "ext-zlib": "Used with compressor/decompressor streams", - "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", - "laminas/laminas-diactoros": "Used with Diactoros Factories", - "slim/slim": "Used with Slim Framework PSR-7 implementation" + "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)" }, "type": "library", "autoload": { "files": [ - "src/filters.php" + "src/functions.php" ], "psr-4": { - "Http\\Message\\": "src/" + "DI\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" - } - ], - "description": "HTTP Message related tools", - "homepage": "http://php-http.org", + "description": "The dependency injection container for humans", + "homepage": "https://php-di.org/", "keywords": [ - "http", - "message", - "psr-7" + "PSR-11", + "container", + "container-interop", + "dependency injection", + "di", + "ioc", + "psr11" ], "support": { - "issues": "https://github.com/php-http/message/issues", - "source": "https://github.com/php-http/message/tree/1.16.1" + "issues": "https://github.com/PHP-DI/PHP-DI/issues", + "source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.7" }, - "time": "2024-03-07T13:22:09+00:00" - }, - { - "name": "php-http/message-factory", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/php-http/message-factory.git", - "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-http/message-factory/zipball/4d8778e1c7d405cbb471574821c1ff5b68cc8f57", - "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57", - "shasum": "" - }, - "require": { - "php": ">=5.4", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ + "funding": [ { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" - } - ], - "description": "Factory interfaces for PSR-7 HTTP Message", - "homepage": "http://php-http.org", - "keywords": [ - "factory", - "http", - "message", - "stream", - "uri" - ], - "support": { - "issues": "https://github.com/php-http/message-factory/issues", - "source": "https://github.com/php-http/message-factory/tree/1.1.0" - }, - "abandoned": "psr/http-factory", - "time": "2023-04-14T14:16:17+00:00" - }, - { - "name": "php-http/promise", - "version": "1.3.1", - "source": { - "type": "git", - "url": "https://github.com/php-http/promise.git", - "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83", - "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3", - "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Http\\Promise\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Joel Wurtz", - "email": "joel.wurtz@gmail.com" + "url": "https://github.com/mnapoli", + "type": "github" }, { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" + "url": "https://tidelift.com/funding/github/packagist/php-di/php-di", + "type": "tidelift" } ], - "description": "Promise used for asynchronous HTTP requests", - "homepage": "http://httplug.io", - "keywords": [ - "promise" - ], - "support": { - "issues": "https://github.com/php-http/promise/issues", - "source": "https://github.com/php-http/promise/tree/1.3.1" - }, - "time": "2024-03-15T13:55:21+00:00" + "time": "2024-07-21T15:55:45+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -6063,16 +5835,16 @@ }, { "name": "phpoption/phpoption", - "version": "1.9.2", + "version": "1.9.3", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "80735db690fe4fc5c76dfa7f9b770634285fa820" + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/80735db690fe4fc5c76dfa7f9b770634285fa820", - "reference": "80735db690fe4fc5c76dfa7f9b770634285fa820", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54", + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54", "shasum": "" }, "require": { @@ -6080,13 +5852,13 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" }, "type": "library", "extra": { "bamarni-bin": { "bin-links": true, - "forward-command": true + "forward-command": false }, "branch-alias": { "dev-master": "1.9-dev" @@ -6122,7 +5894,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.2" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.3" }, "funding": [ { @@ -6134,24 +5906,24 @@ "type": "tidelift" } ], - "time": "2023-11-12T21:59:55+00:00" + "time": "2024-07-20T21:41:07+00:00" }, { "name": "phpseclib/phpseclib", - "version": "3.0.37", + "version": "3.0.42", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8" + "reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/cfa2013d0f68c062055180dd4328cc8b9d1f30b8", - "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/db92f1b1987b12b13f248fe76c3a52cadb67bb98", + "reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98", "shasum": "" }, "require": { - "paragonie/constant_time_encoding": "^1|^2", + "paragonie/constant_time_encoding": "^1|^2|^3", "paragonie/random_compat": "^1.4|^2.0|^9.99.99", "php": ">=5.6.1" }, @@ -6228,7 +6000,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.37" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.42" }, "funding": [ { @@ -6244,20 +6016,20 @@ "type": "tidelift" } ], - "time": "2024-03-03T02:14:58+00:00" + "time": "2024-09-16T03:06:04+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.29.0", + "version": "1.32.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "536889f2b340489d328f5ffb7b02bb6b183ddedc" + "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/536889f2b340489d328f5ffb7b02bb6b183ddedc", - "reference": "536889f2b340489d328f5ffb7b02bb6b183ddedc", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6ca22b154efdd9e3c68c56f5d94670920a1c19a4", + "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4", "shasum": "" }, "require": { @@ -6289,22 +6061,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.32.0" }, - "time": "2024-05-06T12:04:23+00:00" + "time": "2024-09-26T07:23:32+00:00" }, { "name": "phpstan/phpstan", - "version": "1.11.2", + "version": "1.12.6", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "0d5d4294a70deb7547db655c47685d680e39cfec" + "reference": "dc4d2f145a88ea7141ae698effd64d9df46527ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0d5d4294a70deb7547db655c47685d680e39cfec", - "reference": "0d5d4294a70deb7547db655c47685d680e39cfec", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc4d2f145a88ea7141ae698effd64d9df46527ae", + "reference": "dc4d2f145a88ea7141ae698effd64d9df46527ae", "shasum": "" }, "require": { @@ -6349,60 +6121,7 @@ "type": "github" } ], - "time": "2024-05-24T13:23:04+00:00" - }, - { - "name": "pimple/pimple", - "version": "v3.5.0", - "source": { - "type": "git", - "url": "https://github.com/silexphp/Pimple.git", - "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/silexphp/Pimple/zipball/a94b3a4db7fb774b3d78dad2315ddc07629e1bed", - "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/container": "^1.1 || ^2.0" - }, - "require-dev": { - "symfony/phpunit-bridge": "^5.4@dev" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.4.x-dev" - } - }, - "autoload": { - "psr-0": { - "Pimple": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - } - ], - "description": "Pimple, a simple Dependency Injection Container", - "homepage": "https://pimple.symfony.com", - "keywords": [ - "container", - "dependency injection" - ], - "support": { - "source": "https://github.com/silexphp/Pimple/tree/v3.5.0" - }, - "time": "2021-10-28T11:13:42+00:00" + "time": "2024-10-06T15:03:59+00:00" }, { "name": "pion/laravel-chunk-upload", @@ -6516,24 +6235,24 @@ }, { "name": "pragmarx/google2fa", - "version": "v8.0.1", + "version": "v8.0.3", "source": { "type": "git", "url": "https://github.com/antonioribeiro/google2fa.git", - "reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3" + "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/80c3d801b31fe165f8fe99ea085e0a37834e1be3", - "reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", + "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", "shasum": "" }, "require": { - "paragonie/constant_time_encoding": "^1.0|^2.0", + "paragonie/constant_time_encoding": "^1.0|^2.0|^3.0", "php": "^7.1|^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.18", + "phpstan/phpstan": "^1.9", "phpunit/phpunit": "^7.5.15|^8.5|^9.0" }, "type": "library", @@ -6562,9 +6281,9 @@ ], "support": { "issues": "https://github.com/antonioribeiro/google2fa/issues", - "source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.1" + "source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.3" }, - "time": "2022-06-13T21:57:56+00:00" + "time": "2024-09-05T11:56:40+00:00" }, { "name": "psr/cache", @@ -6928,16 +6647,16 @@ }, { "name": "psr/log", - "version": "3.0.0", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { @@ -6972,9 +6691,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.0" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2021-07-14T16:46:02+00:00" + "time": "2024-09-11T13:17:53+00:00" }, { "name": "psr/simple-cache", @@ -7029,16 +6748,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.3", + "version": "v0.12.4", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "b6b6cce7d3ee8fbf31843edce5e8f5a72eff4a73" + "reference": "2fd717afa05341b4f8152547f142cd2f130f6818" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/b6b6cce7d3ee8fbf31843edce5e8f5a72eff4a73", - "reference": "b6b6cce7d3ee8fbf31843edce5e8f5a72eff4a73", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/2fd717afa05341b4f8152547f142cd2f130f6818", + "reference": "2fd717afa05341b4f8152547f142cd2f130f6818", "shasum": "" }, "require": { @@ -7102,22 +6821,22 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.3" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.4" }, - "time": "2024-04-02T15:57:53+00:00" + "time": "2024-06-10T01:18:23+00:00" }, { "name": "purplepixie/phpdns", - "version": "2.1.1", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/purplepixie/phpdns.git", - "reference": "18cd3a43fadcfd16e2789e3c78a264945f6cbfad" + "reference": "2b77de5bb218bc4e5d9c4a4a12bd18fe80a6ab4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/purplepixie/phpdns/zipball/18cd3a43fadcfd16e2789e3c78a264945f6cbfad", - "reference": "18cd3a43fadcfd16e2789e3c78a264945f6cbfad", + "url": "https://api.github.com/repos/purplepixie/phpdns/zipball/2b77de5bb218bc4e5d9c4a4a12bd18fe80a6ab4d", + "reference": "2b77de5bb218bc4e5d9c4a4a12bd18fe80a6ab4d", "shasum": "" }, "require": { @@ -7150,9 +6869,9 @@ "description": "PHP DNS Direct Query Module", "support": { "issues": "https://github.com/purplepixie/phpdns/issues", - "source": "https://github.com/purplepixie/phpdns/tree/2.1.1" + "source": "https://github.com/purplepixie/phpdns/tree/2.2.0" }, - "time": "2024-05-27T13:27:50+00:00" + "time": "2024-09-26T14:39:58+00:00" }, { "name": "pusher/pusher-php-server", @@ -7442,21 +7161,21 @@ }, { "name": "rector/rector", - "version": "1.1.0", + "version": "1.2.6", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "556509e2dcf527369892b7d411379c4a02f31859" + "reference": "6ca85da28159dbd3bb36211c5104b7bc91278e99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/556509e2dcf527369892b7d411379c4a02f31859", - "reference": "556509e2dcf527369892b7d411379c4a02f31859", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/6ca85da28159dbd3bb36211c5104b7bc91278e99", + "reference": "6ca85da28159dbd3bb36211c5104b7bc91278e99", "shasum": "" }, "require": { "php": "^7.2|^8.0", - "phpstan/phpstan": "^1.11" + "phpstan/phpstan": "^1.12.5" }, "conflict": { "rector/rector-doctrine": "*", @@ -7489,7 +7208,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/1.1.0" + "source": "https://github.com/rectorphp/rector/tree/1.2.6" }, "funding": [ { @@ -7497,33 +7216,34 @@ "type": "github" } ], - "time": "2024-05-18T09:40:27+00:00" + "time": "2024-10-03T08:56:44+00:00" }, { "name": "resend/resend-laravel", - "version": "v0.5.0", + "version": "v0.13.0", "source": { "type": "git", "url": "https://github.com/resend/resend-laravel.git", - "reference": "e598d1e25e49a7aa4c35f653d1d828f69ee4fc1d" + "reference": "23aed22df0d0b23c2952da2aaed6a8b88d301a8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/resend/resend-laravel/zipball/e598d1e25e49a7aa4c35f653d1d828f69ee4fc1d", - "reference": "e598d1e25e49a7aa4c35f653d1d828f69ee4fc1d", + "url": "https://api.github.com/repos/resend/resend-laravel/zipball/23aed22df0d0b23c2952da2aaed6a8b88d301a8a", + "reference": "23aed22df0d0b23c2952da2aaed6a8b88d301a8a", "shasum": "" }, "require": { - "illuminate/support": "^9.21|^10.0", + "illuminate/http": "^10.0|^11.0", + "illuminate/support": "^10.0|^11.0", "php": "^8.1", - "resend/resend-php": "^0.7.1", - "symfony/mailer": "^6.2" + "resend/resend-php": "^0.12.0", + "symfony/mailer": "^6.2|^7.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.14", "mockery/mockery": "^1.5", - "orchestra/testbench": "^7.22|^8.0", - "pestphp/pest": "^1.22" + "orchestra/testbench": "^8.17|^9.0", + "pestphp/pest": "^2.0" }, "type": "library", "extra": { @@ -7548,7 +7268,7 @@ "authors": [ { "name": "Resend and contributors", - "homepage": "https://github.com/resendlabs/resend-laravel/contributors" + "homepage": "https://github.com/resend/resend-laravel/contributors" } ], "description": "Resend for Laravel", @@ -7563,22 +7283,22 @@ ], "support": { "issues": "https://github.com/resend/resend-laravel/issues", - "source": "https://github.com/resend/resend-laravel/tree/v0.5.0" + "source": "https://github.com/resend/resend-laravel/tree/v0.13.0" }, - "time": "2023-07-15T17:56:14+00:00" + "time": "2024-07-08T18:51:42+00:00" }, { "name": "resend/resend-php", - "version": "v0.7.2", + "version": "v0.12.0", "source": { "type": "git", "url": "https://github.com/resend/resend-php.git", - "reference": "bef429c2cd43ae1a1d990059c73750d46f249872" + "reference": "37fb79bb8160ce2de521bf37484ba59e89236521" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/resend/resend-php/zipball/bef429c2cd43ae1a1d990059c73750d46f249872", - "reference": "bef429c2cd43ae1a1d990059c73750d46f249872", + "url": "https://api.github.com/repos/resend/resend-php/zipball/37fb79bb8160ce2de521bf37484ba59e89236521", + "reference": "37fb79bb8160ce2de521bf37484ba59e89236521", "shasum": "" }, "require": { @@ -7587,8 +7307,8 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.13", - "pestphp/pest": "^2.0", - "pestphp/pest-plugin-mock": "^2.0" + "mockery/mockery": "^1.6", + "pestphp/pest": "^2.0" }, "type": "library", "autoload": { @@ -7606,7 +7326,7 @@ "authors": [ { "name": "Resend and contributors", - "homepage": "https://github.com/resendlabs/resend-php/contributors" + "homepage": "https://github.com/resend/resend-php/contributors" } ], "description": "Resend PHP library.", @@ -7620,9 +7340,9 @@ ], "support": { "issues": "https://github.com/resend/resend-php/issues", - "source": "https://github.com/resend/resend-php/tree/v0.7.2" + "source": "https://github.com/resend/resend-php/tree/v0.12.0" }, - "time": "2023-09-08T23:47:23+00:00" + "time": "2024-03-04T03:16:28+00:00" }, { "name": "revolt/event-loop", @@ -7696,112 +7416,42 @@ }, "time": "2023-11-30T05:34:44+00:00" }, - { - "name": "sentry/sdk", - "version": "3.6.0", - "source": { - "type": "git", - "url": "https://github.com/getsentry/sentry-php-sdk.git", - "reference": "24c235ff2027401cbea099bf88689e1a1f197c7a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-php-sdk/zipball/24c235ff2027401cbea099bf88689e1a1f197c7a", - "reference": "24c235ff2027401cbea099bf88689e1a1f197c7a", - "shasum": "" - }, - "require": { - "http-interop/http-factory-guzzle": "^1.0", - "sentry/sentry": "^3.22", - "symfony/http-client": "^4.3|^5.0|^6.0|^7.0" - }, - "type": "metapackage", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Sentry", - "email": "accounts@sentry.io" - } - ], - "description": "This is a metapackage shipping sentry/sentry with a recommended HTTP client.", - "homepage": "http://sentry.io", - "keywords": [ - "crash-reporting", - "crash-reports", - "error-handler", - "error-monitoring", - "log", - "logging", - "sentry" - ], - "support": { - "issues": "https://github.com/getsentry/sentry-php-sdk/issues", - "source": "https://github.com/getsentry/sentry-php-sdk/tree/3.6.0" - }, - "funding": [ - { - "url": "https://sentry.io/", - "type": "custom" - }, - { - "url": "https://sentry.io/pricing/", - "type": "custom" - } - ], - "time": "2023-12-04T10:49:33+00:00" - }, { "name": "sentry/sentry", - "version": "3.22.1", + "version": "4.9.0", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-php.git", - "reference": "8859631ba5ab15bc1af420b0eeed19ecc6c9d81d" + "reference": "788ec170f51ebb22f2809a1e3f78b19ccd39b70d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/8859631ba5ab15bc1af420b0eeed19ecc6c9d81d", - "reference": "8859631ba5ab15bc1af420b0eeed19ecc6c9d81d", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/788ec170f51ebb22f2809a1e3f78b19ccd39b70d", + "reference": "788ec170f51ebb22f2809a1e3f78b19ccd39b70d", "shasum": "" }, "require": { + "ext-curl": "*", "ext-json": "*", "ext-mbstring": "*", - "guzzlehttp/promises": "^1.5.3|^2.0", + "guzzlehttp/psr7": "^1.8.4|^2.1.1", "jean85/pretty-package-versions": "^1.5|^2.0.4", "php": "^7.2|^8.0", - "php-http/async-client-implementation": "^1.0", - "php-http/client-common": "^1.5|^2.0", - "php-http/discovery": "^1.15", - "php-http/httplug": "^1.1|^2.0", - "php-http/message": "^1.5", - "php-http/message-factory": "^1.1", - "psr/http-factory": "^1.0", - "psr/http-factory-implementation": "^1.0", "psr/log": "^1.0|^2.0|^3.0", - "symfony/options-resolver": "^3.4.43|^4.4.30|^5.0.11|^6.0|^7.0", - "symfony/polyfill-php80": "^1.17" + "symfony/options-resolver": "^4.4.30|^5.0.11|^6.0|^7.0" }, "conflict": { - "php-http/client-common": "1.8.0", "raven/raven": "*" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.19|3.4.*", + "friendsofphp/php-cs-fixer": "^3.4", + "guzzlehttp/promises": "^1.0|^2.0", "guzzlehttp/psr7": "^1.8.4|^2.1.1", - "http-interop/http-factory-guzzle": "^1.0", "monolog/monolog": "^1.6|^2.0|^3.0", - "nikic/php-parser": "^4.10.3", - "php-http/mock-client": "^1.3", "phpbench/phpbench": "^1.0", - "phpstan/extension-installer": "^1.0", "phpstan/phpstan": "^1.3", - "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpunit": "^8.5.14|^9.4", - "symfony/phpunit-bridge": "^5.2|^6.0", + "symfony/phpunit-bridge": "^5.2|^6.0|^7.0", "vimeo/psalm": "^4.17" }, "suggest": { @@ -7826,7 +7476,7 @@ "email": "accounts@sentry.io" } ], - "description": "A PHP SDK for Sentry (http://sentry.io)", + "description": "PHP SDK for Sentry (http://sentry.io)", "homepage": "http://sentry.io", "keywords": [ "crash-reporting", @@ -7835,11 +7485,13 @@ "error-monitoring", "log", "logging", - "sentry" + "profiling", + "sentry", + "tracing" ], "support": { "issues": "https://github.com/getsentry/sentry-php/issues", - "source": "https://github.com/getsentry/sentry-php/tree/3.22.1" + "source": "https://github.com/getsentry/sentry-php/tree/4.9.0" }, "funding": [ { @@ -7851,47 +7503,42 @@ "type": "custom" } ], - "time": "2023-11-13T11:47:28+00:00" + "time": "2024-08-08T14:40:50+00:00" }, { "name": "sentry/sentry-laravel", - "version": "3.8.2", + "version": "4.9.0", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-laravel.git", - "reference": "1293e5732f8405e12f000cdf5dee78c927a18de0" + "reference": "73078e1f26d57f7a10e3bee2a2f543a02f6493c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/1293e5732f8405e12f000cdf5dee78c927a18de0", - "reference": "1293e5732f8405e12f000cdf5dee78c927a18de0", + "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/73078e1f26d57f7a10e3bee2a2f543a02f6493c3", + "reference": "73078e1f26d57f7a10e3bee2a2f543a02f6493c3", "shasum": "" }, "require": { - "illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0", + "illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0", "nyholm/psr7": "^1.0", "php": "^7.2 | ^8.0", - "sentry/sdk": "^3.4", - "sentry/sentry": "^3.20.1", - "symfony/psr-http-message-bridge": "^1.0 | ^2.0" + "sentry/sentry": "^4.9", + "symfony/psr-http-message-bridge": "^1.0 | ^2.0 | ^6.0 | ^7.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.11", - "laravel/folio": "^1.0", - "laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0", + "guzzlehttp/guzzle": "^7.2", + "laravel/folio": "^1.1", + "laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0", + "livewire/livewire": "^2.0 | ^3.0", "mockery/mockery": "^1.3", - "orchestra/testbench": "^4.7 | ^5.1 | ^6.0 | ^7.0 | ^8.0", + "orchestra/testbench": "^4.7 | ^5.1 | ^6.0 | ^7.0 | ^8.0 | ^9.0", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^8.4 | ^9.3" + "phpunit/phpunit": "^8.4 | ^9.3 | ^10.4" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "3.x-dev", - "dev-2.x": "2.x-dev", - "dev-1.x": "1.x-dev", - "dev-0.x": "0.x-dev" - }, "laravel": { "providers": [ "Sentry\\Laravel\\ServiceProvider", @@ -7927,11 +7574,13 @@ "laravel", "log", "logging", - "sentry" + "profiling", + "sentry", + "tracing" ], "support": { "issues": "https://github.com/getsentry/sentry-laravel/issues", - "source": "https://github.com/getsentry/sentry-laravel/tree/3.8.2" + "source": "https://github.com/getsentry/sentry-laravel/tree/4.9.0" }, "funding": [ { @@ -7943,7 +7592,7 @@ "type": "custom" } ], - "time": "2023-10-12T14:38:46+00:00" + "time": "2024-09-19T12:58:53+00:00" }, { "name": "socialiteproviders/manager", @@ -8072,16 +7721,16 @@ }, { "name": "spatie/backtrace", - "version": "1.6.1", + "version": "1.6.2", "source": { "type": "git", "url": "https://github.com/spatie/backtrace.git", - "reference": "8373b9d51638292e3bfd736a9c19a654111b4a23" + "reference": "1a9a145b044677ae3424693f7b06479fc8c137a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/backtrace/zipball/8373b9d51638292e3bfd736a9c19a654111b4a23", - "reference": "8373b9d51638292e3bfd736a9c19a654111b4a23", + "url": "https://api.github.com/repos/spatie/backtrace/zipball/1a9a145b044677ae3424693f7b06479fc8c137a9", + "reference": "1a9a145b044677ae3424693f7b06479fc8c137a9", "shasum": "" }, "require": { @@ -8119,7 +7768,7 @@ "spatie" ], "support": { - "source": "https://github.com/spatie/backtrace/tree/1.6.1" + "source": "https://github.com/spatie/backtrace/tree/1.6.2" }, "funding": [ { @@ -8131,7 +7780,7 @@ "type": "other" } ], - "time": "2024-04-24T13:22:11+00:00" + "time": "2024-07-22T08:21:24+00:00" }, { "name": "spatie/laravel-activitylog", @@ -8311,16 +7960,16 @@ }, { "name": "spatie/laravel-package-tools", - "version": "1.16.4", + "version": "1.16.5", "source": { "type": "git", "url": "https://github.com/spatie/laravel-package-tools.git", - "reference": "ddf678e78d7f8b17e5cdd99c0c3413a4a6592e53" + "reference": "c7413972cf22ffdff97b68499c22baa04eddb6a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/ddf678e78d7f8b17e5cdd99c0c3413a4a6592e53", - "reference": "ddf678e78d7f8b17e5cdd99c0c3413a4a6592e53", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/c7413972cf22ffdff97b68499c22baa04eddb6a2", + "reference": "c7413972cf22ffdff97b68499c22baa04eddb6a2", "shasum": "" }, "require": { @@ -8359,7 +8008,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-package-tools/issues", - "source": "https://github.com/spatie/laravel-package-tools/tree/1.16.4" + "source": "https://github.com/spatie/laravel-package-tools/tree/1.16.5" }, "funding": [ { @@ -8367,20 +8016,20 @@ "type": "github" } ], - "time": "2024-03-20T07:29:11+00:00" + "time": "2024-08-27T18:56:10+00:00" }, { "name": "spatie/laravel-ray", - "version": "1.36.2", + "version": "1.37.1", "source": { "type": "git", "url": "https://github.com/spatie/laravel-ray.git", - "reference": "1852faa96e5aa6778ea3401ec3176eee77268718" + "reference": "c2bedfd1172648df2c80aaceb2541d70f1d9a5b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-ray/zipball/1852faa96e5aa6778ea3401ec3176eee77268718", - "reference": "1852faa96e5aa6778ea3401ec3176eee77268718", + "url": "https://api.github.com/repos/spatie/laravel-ray/zipball/c2bedfd1172648df2c80aaceb2541d70f1d9a5b9", + "reference": "c2bedfd1172648df2c80aaceb2541d70f1d9a5b9", "shasum": "" }, "require": { @@ -8394,7 +8043,7 @@ "spatie/backtrace": "^1.0", "spatie/ray": "^1.41.1", "symfony/stopwatch": "4.2|^5.1|^6.0|^7.0", - "zbateson/mail-mime-parser": "^1.3.1|^2.0" + "zbateson/mail-mime-parser": "^1.3.1|^2.0|^3.0" }, "require-dev": { "guzzlehttp/guzzle": "^7.3", @@ -8442,7 +8091,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-ray/issues", - "source": "https://github.com/spatie/laravel-ray/tree/1.36.2" + "source": "https://github.com/spatie/laravel-ray/tree/1.37.1" }, "funding": [ { @@ -8454,7 +8103,7 @@ "type": "other" } ], - "time": "2024-05-02T08:26:02+00:00" + "time": "2024-07-12T12:35:17+00:00" }, { "name": "spatie/laravel-schemaless-attributes", @@ -8584,16 +8233,16 @@ }, { "name": "spatie/php-structure-discoverer", - "version": "2.1.1", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/spatie/php-structure-discoverer.git", - "reference": "24f5221641560ec0f7dce23dd814e7d555b0098b" + "reference": "271542206169d95dd2ffe346ddf11f37672553a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/24f5221641560ec0f7dce23dd814e7d555b0098b", - "reference": "24f5221641560ec0f7dce23dd814e7d555b0098b", + "url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/271542206169d95dd2ffe346ddf11f37672553a2", + "reference": "271542206169d95dd2ffe346ddf11f37672553a2", "shasum": "" }, "require": { @@ -8652,7 +8301,7 @@ ], "support": { "issues": "https://github.com/spatie/php-structure-discoverer/issues", - "source": "https://github.com/spatie/php-structure-discoverer/tree/2.1.1" + "source": "https://github.com/spatie/php-structure-discoverer/tree/2.2.0" }, "funding": [ { @@ -8660,7 +8309,7 @@ "type": "github" } ], - "time": "2024-03-13T16:08:30+00:00" + "time": "2024-08-29T10:43:45+00:00" }, { "name": "spatie/ray", @@ -8869,48 +8518,121 @@ "time": "2023-10-16T18:04:12+00:00" }, { - "name": "symfony/console", - "version": "v6.4.7", + "name": "symfony/clock", + "version": "v7.1.1", "source": { "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "a170e64ae10d00ba89e2acbb590dc2e54da8ad8f" + "url": "https://github.com/symfony/clock.git", + "reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/a170e64ae10d00ba89e2acbb590dc2e54da8ad8f", - "reference": "a170e64ae10d00ba89e2acbb590dc2e54da8ad8f", + "url": "https://api.github.com/repos/symfony/clock/zipball/3dfc8b084853586de51dd1441c6242c76a28cbe7", + "reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T14:57:53+00:00" + }, + { + "name": "symfony/console", + "version": "v7.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/0fa539d12b3ccf068a722bbbffa07ca7079af9ee", + "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee", + "shasum": "" + }, + "require": { + "php": ">=8.2", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^5.4|^6.0|^7.0" + "symfony/string": "^6.4|^7.0" }, "conflict": { - "symfony/dependency-injection": "<5.4", - "symfony/dotenv": "<5.4", - "symfony/event-dispatcher": "<5.4", - "symfony/lock": "<5.4", - "symfony/process": "<5.4" + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", "symfony/http-foundation": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -8944,7 +8666,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.7" + "source": "https://github.com/symfony/console/tree/v7.1.5" }, "funding": [ { @@ -8960,20 +8682,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/css-selector", - "version": "v7.0.7", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "b08a4ad89e84b29cec285b7b1f781a7ae51cf4bc" + "reference": "1c7cee86c6f812896af54434f8ce29c8d94f9ff4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/b08a4ad89e84b29cec285b7b1f781a7ae51cf4bc", - "reference": "b08a4ad89e84b29cec285b7b1f781a7ae51cf4bc", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/1c7cee86c6f812896af54434f8ce29c8d94f9ff4", + "reference": "1c7cee86c6f812896af54434f8ce29c8d94f9ff4", "shasum": "" }, "require": { @@ -9009,7 +8731,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.0.7" + "source": "https://github.com/symfony/css-selector/tree/v7.1.1" }, "funding": [ { @@ -9025,7 +8747,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:29:19+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/deprecation-contracts", @@ -9096,22 +8818,22 @@ }, { "name": "symfony/error-handler", - "version": "v6.4.7", + "version": "v7.1.3", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "667a072466c6a53827ed7b119af93806b884cbb3" + "reference": "432bb369952795c61ca1def65e078c4a80dad13c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/667a072466c6a53827ed7b119af93806b884cbb3", - "reference": "667a072466c6a53827ed7b119af93806b884cbb3", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/432bb369952795c61ca1def65e078c4a80dad13c", + "reference": "432bb369952795c61ca1def65e078c4a80dad13c", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "symfony/var-dumper": "^6.4|^7.0" }, "conflict": { "symfony/deprecation-contracts": "<2.5", @@ -9120,7 +8842,7 @@ "require-dev": { "symfony/deprecation-contracts": "^2.5|^3", "symfony/http-kernel": "^6.4|^7.0", - "symfony/serializer": "^5.4|^6.0|^7.0" + "symfony/serializer": "^6.4|^7.0" }, "bin": [ "Resources/bin/patch-type-declarations" @@ -9151,7 +8873,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.4.7" + "source": "https://github.com/symfony/error-handler/tree/v7.1.3" }, "funding": [ { @@ -9167,20 +8889,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-07-26T13:02:51+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.0.7", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "db2a7fab994d67d92356bb39c367db115d9d30f9" + "reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/db2a7fab994d67d92356bb39c367db115d9d30f9", - "reference": "db2a7fab994d67d92356bb39c367db115d9d30f9", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7", + "reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7", "shasum": "" }, "require": { @@ -9231,7 +8953,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.0.7" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.1.1" }, "funding": [ { @@ -9247,7 +8969,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:29:19+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -9327,23 +9049,23 @@ }, { "name": "symfony/finder", - "version": "v6.4.7", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "511c48990be17358c23bf45c5d71ab85d40fb764" + "reference": "d95bbf319f7d052082fb7af147e0f835a695e823" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/511c48990be17358c23bf45c5d71ab85d40fb764", - "reference": "511c48990be17358c23bf45c5d71ab85d40fb764", + "url": "https://api.github.com/repos/symfony/finder/zipball/d95bbf319f7d052082fb7af147e0f835a695e823", + "reference": "d95bbf319f7d052082fb7af147e0f835a695e823", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.0|^7.0" + "symfony/filesystem": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -9371,7 +9093,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.7" + "source": "https://github.com/symfony/finder/tree/v7.1.4" }, "funding": [ { @@ -9387,211 +9109,40 @@ "type": "tidelift" } ], - "time": "2024-04-23T10:36:43+00:00" - }, - { - "name": "symfony/http-client", - "version": "v6.4.7", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-client.git", - "reference": "3683d8107cf1efdd24795cc5f7482be1eded34ac" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/3683d8107cf1efdd24795cc5f7482be1eded34ac", - "reference": "3683d8107cf1efdd24795cc5f7482be1eded34ac", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "^3.4.1", - "symfony/service-contracts": "^2.5|^3" - }, - "conflict": { - "php-http/discovery": "<1.15", - "symfony/http-foundation": "<6.3" - }, - "provide": { - "php-http/async-client-implementation": "*", - "php-http/client-implementation": "*", - "psr/http-client-implementation": "1.0", - "symfony/http-client-implementation": "3.0" - }, - "require-dev": { - "amphp/amp": "^2.5", - "amphp/http-client": "^4.2.1", - "amphp/http-tunnel": "^1.0", - "amphp/socket": "^1.1", - "guzzlehttp/promises": "^1.4|^2.0", - "nyholm/psr7": "^1.0", - "php-http/httplug": "^1.0|^2.0", - "psr/http-client": "^1.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\HttpClient\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", - "homepage": "https://symfony.com", - "keywords": [ - "http" - ], - "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.7" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-04-18T09:22:46+00:00" - }, - { - "name": "symfony/http-client-contracts", - "version": "v3.5.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "20414d96f391677bf80078aa55baece78b82647d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", - "reference": "20414d96f391677bf80078aa55baece78b82647d", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\HttpClient\\": "" - }, - "exclude-from-classmap": [ - "/Test/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to HTTP clients", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-08-13T14:28:19+00:00" }, { "name": "symfony/http-foundation", - "version": "v6.4.7", + "version": "v7.1.7", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "b4db6b833035477cb70e18d0ae33cb7c2b521759" + "reference": "5183b61657807099d98f3367bcccb850238b17a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/b4db6b833035477cb70e18d0ae33cb7c2b521759", - "reference": "b4db6b833035477cb70e18d0ae33cb7c2b521759", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5183b61657807099d98f3367bcccb850238b17a9", + "reference": "5183b61657807099d98f3367bcccb850238b17a9", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.2", "symfony/polyfill-mbstring": "~1.1", "symfony/polyfill-php83": "^1.27" }, "conflict": { - "symfony/cache": "<6.3" + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4" }, "require-dev": { - "doctrine/dbal": "^2.13.1|^3|^4", + "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.3|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", - "symfony/mime": "^5.4|^6.0|^7.0", - "symfony/rate-limiter": "^5.4|^6.0|^7.0" + "symfony/cache": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -9619,7 +9170,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.4.7" + "source": "https://github.com/symfony/http-foundation/tree/v7.1.7" }, "funding": [ { @@ -9635,77 +9186,77 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-11-06T09:02:46+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.4.7", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "b7b5e6cdef670a0c82d015a966ffc7e855861a98" + "reference": "44204d96150a9df1fc57601ec933d23fefc2d65b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b7b5e6cdef670a0c82d015a966ffc7e855861a98", - "reference": "b7b5e6cdef670a0c82d015a966ffc7e855861a98", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/44204d96150a9df1fc57601ec933d23fefc2d65b", + "reference": "44204d96150a9df1fc57601ec933d23fefc2d65b", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", "symfony/error-handler": "^6.4|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", "symfony/http-foundation": "^6.4|^7.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/browser-kit": "<5.4", - "symfony/cache": "<5.4", - "symfony/config": "<6.1", - "symfony/console": "<5.4", + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", "symfony/dependency-injection": "<6.4", - "symfony/doctrine-bridge": "<5.4", - "symfony/form": "<5.4", - "symfony/http-client": "<5.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", "symfony/http-client-contracts": "<2.5", - "symfony/mailer": "<5.4", - "symfony/messenger": "<5.4", - "symfony/translation": "<5.4", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", "symfony/translation-contracts": "<2.5", - "symfony/twig-bridge": "<5.4", + "symfony/twig-bridge": "<6.4", "symfony/validator": "<6.4", - "symfony/var-dumper": "<6.3", - "twig/twig": "<2.13" + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.0.4" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^5.4|^6.0|^7.0", - "symfony/clock": "^6.2|^7.0", - "symfony/config": "^6.1|^7.0", - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/css-selector": "^5.4|^6.0|^7.0", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", "symfony/dependency-injection": "^6.4|^7.0", - "symfony/dom-crawler": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/dom-crawler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/property-access": "^5.4.5|^6.0.5|^7.0", - "symfony/routing": "^5.4|^6.0|^7.0", - "symfony/serializer": "^6.4.4|^7.0.4", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/translation": "^5.4|^6.0|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^7.1", + "symfony/routing": "^6.4|^7.0", + "symfony/serializer": "^7.1", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/uid": "^6.4|^7.0", "symfony/validator": "^6.4|^7.0", - "symfony/var-dumper": "^5.4|^6.4|^7.0", - "symfony/var-exporter": "^6.2|^7.0", - "twig/twig": "^2.13|^3.0.4" + "symfony/var-dumper": "^6.4|^7.0", + "symfony/var-exporter": "^6.4|^7.0", + "twig/twig": "^3.0.4" }, "type": "library", "autoload": { @@ -9733,7 +9284,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.7" + "source": "https://github.com/symfony/http-kernel/tree/v7.1.5" }, "funding": [ { @@ -9749,43 +9300,43 @@ "type": "tidelift" } ], - "time": "2024-04-29T11:24:44+00:00" + "time": "2024-09-21T06:09:21+00:00" }, { "name": "symfony/mailer", - "version": "v6.4.7", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "2c446d4e446995bed983c0b5bb9ff837e8de7dbd" + "reference": "bbf21460c56f29810da3df3e206e38dfbb01e80b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/2c446d4e446995bed983c0b5bb9ff837e8de7dbd", - "reference": "2c446d4e446995bed983c0b5bb9ff837e8de7dbd", + "url": "https://api.github.com/repos/symfony/mailer/zipball/bbf21460c56f29810da3df3e206e38dfbb01e80b", + "reference": "bbf21460c56f29810da3df3e206e38dfbb01e80b", "shasum": "" }, "require": { "egulias/email-validator": "^2.1.10|^3|^4", - "php": ">=8.1", + "php": ">=8.2", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", - "symfony/mime": "^6.2|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", "symfony/service-contracts": "^2.5|^3" }, "conflict": { "symfony/http-client-contracts": "<2.5", - "symfony/http-kernel": "<5.4", - "symfony/messenger": "<6.2", - "symfony/mime": "<6.2", - "symfony/twig-bridge": "<6.2.1" + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" }, "require-dev": { - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/http-client": "^5.4|^6.0|^7.0", - "symfony/messenger": "^6.2|^7.0", - "symfony/twig-bridge": "^6.2|^7.0" + "symfony/console": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/twig-bridge": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -9813,7 +9364,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v6.4.7" + "source": "https://github.com/symfony/mailer/tree/v7.1.5" }, "funding": [ { @@ -9829,25 +9380,24 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-09-08T12:32:26+00:00" }, { "name": "symfony/mime", - "version": "v6.4.7", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "decadcf3865918ecfcbfa90968553994ce935a5e" + "reference": "caa1e521edb2650b8470918dfe51708c237f0598" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/decadcf3865918ecfcbfa90968553994ce935a5e", - "reference": "decadcf3865918ecfcbfa90968553994ce935a5e", + "url": "https://api.github.com/repos/symfony/mime/zipball/caa1e521edb2650b8470918dfe51708c237f0598", + "reference": "caa1e521edb2650b8470918dfe51708c237f0598", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.2", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, @@ -9855,18 +9405,18 @@ "egulias/email-validator": "~3.0.0", "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", - "symfony/mailer": "<5.4", - "symfony/serializer": "<6.3.2" + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.4|^7.0", - "symfony/property-access": "^5.4|^6.0|^7.0", - "symfony/property-info": "^5.4|^6.0|^7.0", - "symfony/serializer": "^6.3.2|^7.0" + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" }, "type": "library", "autoload": { @@ -9898,7 +9448,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v6.4.7" + "source": "https://github.com/symfony/mime/tree/v7.1.6" }, "funding": [ { @@ -9914,20 +9464,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-10-25T15:11:02+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.0.7", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "23cc173858776ad451e31f053b1c9f47840b2cfa" + "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/23cc173858776ad451e31f053b1c9f47840b2cfa", - "reference": "23cc173858776ad451e31f053b1c9f47840b2cfa", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/47aa818121ed3950acd2b58d1d37d08a94f9bf55", + "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55", "shasum": "" }, "require": { @@ -9965,7 +9515,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.0.7" + "source": "https://github.com/symfony/options-resolver/tree/v7.1.1" }, "funding": [ { @@ -9981,24 +9531,24 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:29:19+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-ctype": "*" @@ -10044,7 +9594,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" }, "funding": [ { @@ -10060,24 +9610,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-iconv", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-iconv.git", - "reference": "cd4226d140ecd3d0f13d32ed0a4a095ffe871d2f" + "reference": "48becf00c920479ca2e910c22a5a39e5d47ca956" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/cd4226d140ecd3d0f13d32ed0a4a095ffe871d2f", - "reference": "cd4226d140ecd3d0f13d32ed0a4a095ffe871d2f", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/48becf00c920479ca2e910c22a5a39e5d47ca956", + "reference": "48becf00c920479ca2e910c22a5a39e5d47ca956", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-iconv": "*" @@ -10124,7 +9674,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-iconv/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-iconv/tree/v1.31.0" }, "funding": [ { @@ -10140,24 +9690,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -10202,7 +9752,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" }, "funding": [ { @@ -10218,26 +9768,25 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "a287ed7475f85bf6f61890146edbc932c0fff919" + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a287ed7475f85bf6f61890146edbc932c0fff919", - "reference": "a287ed7475f85bf6f61890146edbc932c0fff919", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773", + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773", "shasum": "" }, "require": { - "php": ">=7.1", - "symfony/polyfill-intl-normalizer": "^1.10", - "symfony/polyfill-php72": "^1.10" + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" }, "suggest": { "ext-intl": "For best performance" @@ -10286,7 +9835,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0" }, "funding": [ { @@ -10302,24 +9851,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -10367,7 +9916,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" }, "funding": [ { @@ -10383,24 +9932,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-mbstring": "*" @@ -10447,7 +9996,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" }, "funding": [ { @@ -10463,97 +10012,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" - }, - { - "name": "symfony/polyfill-php72", - "version": "v1.29.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/861391a8da9a04cbad2d232ddd9e4893220d6e25", - "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.29.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -10600,7 +10076,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" }, "funding": [ { @@ -10616,25 +10092,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "86fcae159633351e5fd145d1c47de6c528f8caff" + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/86fcae159633351e5fd145d1c47de6c528f8caff", - "reference": "86fcae159633351e5fd145d1c47de6c528f8caff", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", "shasum": "" }, "require": { - "php": ">=7.1", - "symfony/polyfill-php80": "^1.14" + "php": ">=7.2" }, "type": "library", "extra": { @@ -10677,7 +10152,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" }, "funding": [ { @@ -10693,24 +10168,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-uuid", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", - "reference": "3abdd21b0ceaa3000ee950097bc3cf9efc137853" + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/3abdd21b0ceaa3000ee950097bc3cf9efc137853", - "reference": "3abdd21b0ceaa3000ee950097bc3cf9efc137853", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-uuid": "*" @@ -10756,7 +10231,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.31.0" }, "funding": [ { @@ -10772,24 +10247,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/process", - "version": "v6.4.7", + "version": "v7.1.7", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "cdb1c81c145fd5aa9b0038bab694035020943381" + "reference": "9b8a40b7289767aa7117e957573c2a535efe6585" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/cdb1c81c145fd5aa9b0038bab694035020943381", - "reference": "cdb1c81c145fd5aa9b0038bab694035020943381", + "url": "https://api.github.com/repos/symfony/process/zipball/9b8a40b7289767aa7117e957573c2a535efe6585", + "reference": "9b8a40b7289767aa7117e957573c2a535efe6585", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "type": "library", "autoload": { @@ -10817,7 +10292,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.7" + "source": "https://github.com/symfony/process/tree/v7.1.7" }, "funding": [ { @@ -10833,47 +10308,42 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-11-06T09:25:12+00:00" }, { "name": "symfony/psr-http-message-bridge", - "version": "v2.3.1", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e" + "reference": "405a7bcd872f1563966f64be19f1362d94ce71ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/581ca6067eb62640de5ff08ee1ba6850a0ee472e", - "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/405a7bcd872f1563966f64be19f1362d94ce71ab", + "reference": "405a7bcd872f1563966f64be19f1362d94ce71ab", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/http-message": "^1.0 || ^2.0", - "symfony/deprecation-contracts": "^2.5 || ^3.0", - "symfony/http-foundation": "^5.4 || ^6.0" + "php": ">=8.2", + "psr/http-message": "^1.0|^2.0", + "symfony/http-foundation": "^6.4|^7.0" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-kernel": "<6.4" }, "require-dev": { "nyholm/psr7": "^1.1", - "psr/log": "^1.1 || ^2 || ^3", - "symfony/browser-kit": "^5.4 || ^6.0", - "symfony/config": "^5.4 || ^6.0", - "symfony/event-dispatcher": "^5.4 || ^6.0", - "symfony/framework-bundle": "^5.4 || ^6.0", - "symfony/http-kernel": "^5.4 || ^6.0", - "symfony/phpunit-bridge": "^6.2" - }, - "suggest": { - "nyholm/psr7": "For a super lightweight PSR-7/17 implementation" + "php-http/discovery": "^1.15", + "psr/log": "^1.1.4|^2|^3", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0" }, "type": "symfony-bridge", - "extra": { - "branch-alias": { - "dev-main": "2.3-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Bridge\\PsrHttpMessage\\": "" @@ -10893,11 +10363,11 @@ }, { "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" + "homepage": "https://symfony.com/contributors" } ], "description": "PSR HTTP message bridge", - "homepage": "http://symfony.com", + "homepage": "https://symfony.com", "keywords": [ "http", "http-message", @@ -10905,8 +10375,7 @@ "psr-7" ], "support": { - "issues": "https://github.com/symfony/psr-http-message-bridge/issues", - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v2.3.1" + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.1.4" }, "funding": [ { @@ -10922,40 +10391,38 @@ "type": "tidelift" } ], - "time": "2023-07-26T11:53:26+00:00" + "time": "2024-08-15T22:48:53+00:00" }, { "name": "symfony/routing", - "version": "v6.4.7", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "276e06398f71fa2a973264d94f28150f93cfb907" + "reference": "1500aee0094a3ce1c92626ed8cf3c2037e86f5a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/276e06398f71fa2a973264d94f28150f93cfb907", - "reference": "276e06398f71fa2a973264d94f28150f93cfb907", + "url": "https://api.github.com/repos/symfony/routing/zipball/1500aee0094a3ce1c92626ed8cf3c2037e86f5a7", + "reference": "1500aee0094a3ce1c92626ed8cf3c2037e86f5a7", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { - "doctrine/annotations": "<1.12", - "symfony/config": "<6.2", - "symfony/dependency-injection": "<5.4", - "symfony/yaml": "<5.4" + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" }, "require-dev": { - "doctrine/annotations": "^1.12|^2", "psr/log": "^1|^2|^3", - "symfony/config": "^6.2|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^5.4|^6.0|^7.0", - "symfony/yaml": "^5.4|^6.0|^7.0" + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -10989,7 +10456,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.4.7" + "source": "https://github.com/symfony/routing/tree/v7.1.4" }, "funding": [ { @@ -11005,7 +10472,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-08-29T08:16:25+00:00" }, { "name": "symfony/service-contracts", @@ -11092,16 +10559,16 @@ }, { "name": "symfony/stopwatch", - "version": "v7.0.7", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "41a7a24aa1dc82adf46a06bc292d1923acfe6b84" + "reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/41a7a24aa1dc82adf46a06bc292d1923acfe6b84", - "reference": "41a7a24aa1dc82adf46a06bc292d1923acfe6b84", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d", + "reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d", "shasum": "" }, "require": { @@ -11134,7 +10601,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v7.0.7" + "source": "https://github.com/symfony/stopwatch/tree/v7.1.1" }, "funding": [ { @@ -11150,20 +10617,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:29:19+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/string", - "version": "v7.0.7", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "e405b5424dc2528e02e31ba26b83a79fd4eb8f63" + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/e405b5424dc2528e02e31ba26b83a79fd4eb8f63", - "reference": "e405b5424dc2528e02e31ba26b83a79fd4eb8f63", + "url": "https://api.github.com/repos/symfony/string/zipball/d66f9c343fa894ec2037cc928381df90a7ad4306", + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306", "shasum": "" }, "require": { @@ -11177,6 +10644,7 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { + "symfony/emoji": "^7.1", "symfony/error-handler": "^6.4|^7.0", "symfony/http-client": "^6.4|^7.0", "symfony/intl": "^6.4|^7.0", @@ -11220,7 +10688,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.0.7" + "source": "https://github.com/symfony/string/tree/v7.1.5" }, "funding": [ { @@ -11236,37 +10704,36 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:29:19+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/translation", - "version": "v6.4.7", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "7495687c58bfd88b7883823747b0656d90679123" + "reference": "235535e3f84f3dfbdbde0208ede6ca75c3a489ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/7495687c58bfd88b7883823747b0656d90679123", - "reference": "7495687c58bfd88b7883823747b0656d90679123", + "url": "https://api.github.com/repos/symfony/translation/zipball/235535e3f84f3dfbdbde0208ede6ca75c3a489ea", + "reference": "235535e3f84f3dfbdbde0208ede6ca75c3a489ea", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.2", "symfony/polyfill-mbstring": "~1.0", "symfony/translation-contracts": "^2.5|^3.0" }, "conflict": { - "symfony/config": "<5.4", - "symfony/console": "<5.4", - "symfony/dependency-injection": "<5.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", "symfony/http-client-contracts": "<2.5", - "symfony/http-kernel": "<5.4", + "symfony/http-kernel": "<6.4", "symfony/service-contracts": "<2.5", - "symfony/twig-bundle": "<5.4", - "symfony/yaml": "<5.4" + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" }, "provide": { "symfony/translation-implementation": "2.3|3.0" @@ -11274,17 +10741,17 @@ "require-dev": { "nikic/php-parser": "^4.18|^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0", - "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/routing": "^6.4|^7.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^5.4|^6.0|^7.0" + "symfony/yaml": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -11315,7 +10782,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v6.4.7" + "source": "https://github.com/symfony/translation/tree/v7.1.5" }, "funding": [ { @@ -11331,7 +10798,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-09-16T06:30:38+00:00" }, { "name": "symfony/translation-contracts", @@ -11413,24 +10880,24 @@ }, { "name": "symfony/uid", - "version": "v6.4.7", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "a66efcb71d8bc3a207d9d78e0bd67f3321510355" + "reference": "8c7bb8acb933964055215d89f9a9871df0239317" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/a66efcb71d8bc3a207d9d78e0bd67f3321510355", - "reference": "a66efcb71d8bc3a207d9d78e0bd67f3321510355", + "url": "https://api.github.com/repos/symfony/uid/zipball/8c7bb8acb933964055215d89f9a9871df0239317", + "reference": "8c7bb8acb933964055215d89f9a9871df0239317", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/polyfill-uuid": "^1.15" }, "require-dev": { - "symfony/console": "^5.4|^6.0|^7.0" + "symfony/console": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -11467,7 +10934,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v6.4.7" + "source": "https://github.com/symfony/uid/tree/v7.1.5" }, "funding": [ { @@ -11483,38 +10950,36 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-09-17T09:16:35+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.4.7", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "7a9cd977cd1c5fed3694bee52990866432af07d7" + "reference": "e20e03889539fd4e4211e14d2179226c513c010d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/7a9cd977cd1c5fed3694bee52990866432af07d7", - "reference": "7a9cd977cd1c5fed3694bee52990866432af07d7", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/e20e03889539fd4e4211e14d2179226c513c010d", + "reference": "e20e03889539fd4e4211e14d2179226c513c010d", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.2", "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "symfony/console": "<5.4" + "symfony/console": "<6.4" }, "require-dev": { "ext-iconv": "*", - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/error-handler": "^6.3|^7.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/uid": "^5.4|^6.0|^7.0", - "twig/twig": "^2.13|^3.0.4" + "symfony/console": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0", + "twig/twig": "^3.0.4" }, "bin": [ "Resources/bin/var-dump-server" @@ -11552,7 +11017,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.7" + "source": "https://github.com/symfony/var-dumper/tree/v7.1.5" }, "funding": [ { @@ -11568,20 +11033,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-09-16T10:07:02+00:00" }, { "name": "symfony/yaml", - "version": "v6.4.7", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "53e8b1ef30a65f78eac60fddc5ee7ebbbdb1dee0" + "reference": "762ee56b2649659380e0ef4d592d807bc17b7971" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/53e8b1ef30a65f78eac60fddc5ee7ebbbdb1dee0", - "reference": "53e8b1ef30a65f78eac60fddc5ee7ebbbdb1dee0", + "url": "https://api.github.com/repos/symfony/yaml/zipball/762ee56b2649659380e0ef4d592d807bc17b7971", + "reference": "762ee56b2649659380e0ef4d592d807bc17b7971", "shasum": "" }, "require": { @@ -11624,7 +11089,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.7" + "source": "https://github.com/symfony/yaml/tree/v6.4.12" }, "funding": [ { @@ -11640,7 +11105,7 @@ "type": "tidelift" } ], - "time": "2024-04-28T10:28:08+00:00" + "time": "2024-09-17T12:47:12+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -11756,23 +11221,23 @@ }, { "name": "vlucas/phpdotenv", - "version": "v5.6.0", + "version": "v5.6.1", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4" + "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4", - "reference": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2", + "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2", "shasum": "" }, "require": { "ext-pcre": "*", - "graham-campbell/result-type": "^1.1.2", + "graham-campbell/result-type": "^1.1.3", "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.2", + "phpoption/phpoption": "^1.9.3", "symfony/polyfill-ctype": "^1.24", "symfony/polyfill-mbstring": "^1.24", "symfony/polyfill-php80": "^1.24" @@ -11789,7 +11254,7 @@ "extra": { "bamarni-bin": { "bin-links": true, - "forward-command": true + "forward-command": false }, "branch-alias": { "dev-master": "5.6-dev" @@ -11824,7 +11289,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.0" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1" }, "funding": [ { @@ -11836,7 +11301,7 @@ "type": "tidelift" } ], - "time": "2023-11-12T22:43:29+00:00" + "time": "2024-07-20T21:52:34+00:00" }, { "name": "voku/portable-ascii", @@ -12082,30 +11547,31 @@ }, { "name": "zbateson/mail-mime-parser", - "version": "2.4.1", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/zbateson/mail-mime-parser.git", - "reference": "ff49e02f6489b38f7cc3d1bd3971adc0f872569c" + "reference": "e0d4423fe27850c9dd301190767dbc421acc2f19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zbateson/mail-mime-parser/zipball/ff49e02f6489b38f7cc3d1bd3971adc0f872569c", - "reference": "ff49e02f6489b38f7cc3d1bd3971adc0f872569c", + "url": "https://api.github.com/repos/zbateson/mail-mime-parser/zipball/e0d4423fe27850c9dd301190767dbc421acc2f19", + "reference": "e0d4423fe27850c9dd301190767dbc421acc2f19", "shasum": "" }, "require": { - "guzzlehttp/psr7": "^1.7.0|^2.0", - "php": ">=7.1", - "pimple/pimple": "^3.0", - "zbateson/mb-wrapper": "^1.0.1", - "zbateson/stream-decorators": "^1.0.6" + "guzzlehttp/psr7": "^2.5", + "php": ">=8.0", + "php-di/php-di": "^6.0|^7.0", + "psr/log": "^1|^2|^3", + "zbateson/mb-wrapper": "^2.0", + "zbateson/stream-decorators": "^2.1" }, "require-dev": { "friendsofphp/php-cs-fixer": "*", - "mikey179/vfsstream": "^1.6.0", + "monolog/monolog": "^2|^3", "phpstan/phpstan": "*", - "phpunit/phpunit": "<10" + "phpunit/phpunit": "^9.6" }, "suggest": { "ext-iconv": "For best support/performance", @@ -12153,24 +11619,24 @@ "type": "github" } ], - "time": "2024-04-28T00:58:54+00:00" + "time": "2024-08-10T18:44:09+00:00" }, { "name": "zbateson/mb-wrapper", - "version": "1.2.1", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/zbateson/mb-wrapper.git", - "reference": "09a8b77eb94af3823a9a6623dcc94f8d988da67f" + "reference": "9e4373a153585d12b6c621ac4a6bb143264d4619" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zbateson/mb-wrapper/zipball/09a8b77eb94af3823a9a6623dcc94f8d988da67f", - "reference": "09a8b77eb94af3823a9a6623dcc94f8d988da67f", + "url": "https://api.github.com/repos/zbateson/mb-wrapper/zipball/9e4373a153585d12b6c621ac4a6bb143264d4619", + "reference": "9e4373a153585d12b6c621ac4a6bb143264d4619", "shasum": "" }, "require": { - "php": ">=7.1", + "php": ">=8.0", "symfony/polyfill-iconv": "^1.9", "symfony/polyfill-mbstring": "^1.9" }, @@ -12214,7 +11680,7 @@ ], "support": { "issues": "https://github.com/zbateson/mb-wrapper/issues", - "source": "https://github.com/zbateson/mb-wrapper/tree/1.2.1" + "source": "https://github.com/zbateson/mb-wrapper/tree/2.0.0" }, "funding": [ { @@ -12222,31 +11688,31 @@ "type": "github" } ], - "time": "2024-03-18T04:31:04+00:00" + "time": "2024-03-20T01:38:07+00:00" }, { "name": "zbateson/stream-decorators", - "version": "1.2.1", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/zbateson/stream-decorators.git", - "reference": "783b034024fda8eafa19675fb2552f8654d3a3e9" + "reference": "32a2a62fb0f26313395c996ebd658d33c3f9c4e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zbateson/stream-decorators/zipball/783b034024fda8eafa19675fb2552f8654d3a3e9", - "reference": "783b034024fda8eafa19675fb2552f8654d3a3e9", + "url": "https://api.github.com/repos/zbateson/stream-decorators/zipball/32a2a62fb0f26313395c996ebd658d33c3f9c4e5", + "reference": "32a2a62fb0f26313395c996ebd658d33c3f9c4e5", "shasum": "" }, "require": { - "guzzlehttp/psr7": "^1.9 | ^2.0", - "php": ">=7.2", - "zbateson/mb-wrapper": "^1.0.0" + "guzzlehttp/psr7": "^2.5", + "php": ">=8.0", + "zbateson/mb-wrapper": "^2.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "*", "phpstan/phpstan": "*", - "phpunit/phpunit": "<10.0" + "phpunit/phpunit": "^9.6|^10.0" }, "type": "library", "autoload": { @@ -12277,7 +11743,7 @@ ], "support": { "issues": "https://github.com/zbateson/stream-decorators/issues", - "source": "https://github.com/zbateson/stream-decorators/tree/1.2.1" + "source": "https://github.com/zbateson/stream-decorators/tree/2.1.1" }, "funding": [ { @@ -12285,10 +11751,175 @@ "type": "github" } ], - "time": "2023-05-30T22:51:52+00:00" + "time": "2024-04-29T21:42:39+00:00" + }, + { + "name": "zircote/swagger-php", + "version": "4.11.0", + "source": { + "type": "git", + "url": "https://github.com/zircote/swagger-php.git", + "reference": "3b6f3800f4fd6544ada4dce180c6b69eaead7c7c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/3b6f3800f4fd6544ada4dce180c6b69eaead7c7c", + "reference": "3b6f3800f4fd6544ada4dce180c6b69eaead7c7c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.2", + "psr/log": "^1.1 || ^2.0 || ^3.0", + "symfony/deprecation-contracts": "^2 || ^3", + "symfony/finder": ">=2.2", + "symfony/yaml": ">=3.3" + }, + "require-dev": { + "composer/package-versions-deprecated": "^1.11", + "doctrine/annotations": "^1.7 || ^2.0", + "friendsofphp/php-cs-fixer": "^2.17 || 3.62.0", + "phpstan/phpstan": "^1.6", + "phpunit/phpunit": ">=8", + "vimeo/psalm": "^4.23" + }, + "suggest": { + "doctrine/annotations": "^1.7 || ^2.0" + }, + "bin": [ + "bin/openapi" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "OpenApi\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Robert Allen", + "email": "zircote@gmail.com" + }, + { + "name": "Bob Fanger", + "email": "bfanger@gmail.com", + "homepage": "https://bfanger.nl" + }, + { + "name": "Martin Rademacher", + "email": "mano@radebatz.net", + "homepage": "https://radebatz.net" + } + ], + "description": "swagger-php - Generate interactive documentation for your RESTful API using phpdoc annotations", + "homepage": "https://github.com/zircote/swagger-php/", + "keywords": [ + "api", + "json", + "rest", + "service discovery" + ], + "support": { + "issues": "https://github.com/zircote/swagger-php/issues", + "source": "https://github.com/zircote/swagger-php/tree/4.11.0" + }, + "time": "2024-10-09T03:11:12+00:00" } ], "packages-dev": [ + { + "name": "barryvdh/laravel-debugbar", + "version": "v3.14.3", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-debugbar.git", + "reference": "c0bee7c08ae2429e4a9ed2bc75679b012db6e3bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/c0bee7c08ae2429e4a9ed2bc75679b012db6e3bd", + "reference": "c0bee7c08ae2429e4a9ed2bc75679b012db6e3bd", + "shasum": "" + }, + "require": { + "illuminate/routing": "^9|^10|^11", + "illuminate/session": "^9|^10|^11", + "illuminate/support": "^9|^10|^11", + "maximebf/debugbar": "~1.23.0", + "php": "^8.0", + "symfony/finder": "^6|^7" + }, + "require-dev": { + "mockery/mockery": "^1.3.3", + "orchestra/testbench-dusk": "^5|^6|^7|^8|^9", + "phpunit/phpunit": "^9.6|^10.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.14-dev" + }, + "laravel": { + "providers": [ + "Barryvdh\\Debugbar\\ServiceProvider" + ], + "aliases": { + "Debugbar": "Barryvdh\\Debugbar\\Facades\\Debugbar" + } + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Barryvdh\\Debugbar\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "PHP Debugbar integration for Laravel", + "keywords": [ + "debug", + "debugbar", + "laravel", + "profiler", + "webprofiler" + ], + "support": { + "issues": "https://github.com/barryvdh/laravel-debugbar/issues", + "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.14.3" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2024-10-02T09:17:49+00:00" + }, { "name": "brianium/paratest", "version": "v7.4.3", @@ -12448,16 +12079,16 @@ }, { "name": "fidry/cpu-core-counter", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42" + "reference": "8520451a140d3f46ac33042715115e290cf5785f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/f92996c4d5c1a696a6a970e20f7c4216200fcc42", - "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", "shasum": "" }, "require": { @@ -12497,7 +12128,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.1.0" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" }, "funding": [ { @@ -12505,30 +12136,30 @@ "type": "github" } ], - "time": "2024-02-07T09:43:46+00:00" + "time": "2024-08-06T10:04:20+00:00" }, { "name": "filp/whoops", - "version": "2.15.4", + "version": "2.16.0", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "a139776fa3f5985a50b509f2a02ff0f709d2a546" + "reference": "befcdc0e5dce67252aa6322d82424be928214fa2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/a139776fa3f5985a50b509f2a02ff0f709d2a546", - "reference": "a139776fa3f5985a50b509f2a02ff0f709d2a546", + "url": "https://api.github.com/repos/filp/whoops/zipball/befcdc0e5dce67252aa6322d82424be928214fa2", + "reference": "befcdc0e5dce67252aa6322d82424be928214fa2", "shasum": "" }, "require": { - "php": "^5.5.9 || ^7.0 || ^8.0", + "php": "^7.1 || ^8.0", "psr/log": "^1.0.1 || ^2.0 || ^3.0" }, "require-dev": { - "mockery/mockery": "^0.9 || ^1.0", - "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3", - "symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0" + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" }, "suggest": { "symfony/var-dumper": "Pretty print complex values better with var-dumper available", @@ -12568,7 +12199,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.15.4" + "source": "https://github.com/filp/whoops/tree/2.16.0" }, "funding": [ { @@ -12576,7 +12207,7 @@ "type": "github" } ], - "time": "2023-11-03T12:00:00+00:00" + "time": "2024-09-25T12:00:00+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -12631,47 +12262,43 @@ }, { "name": "laravel/dusk", - "version": "v7.13.0", + "version": "v8.2.8", "source": { "type": "git", "url": "https://github.com/laravel/dusk.git", - "reference": "dce7c4cc1c308bb18e95b2b3bf7d06d3f040a1f6" + "reference": "5bff1e8dd87ec653a2202475377152e5d14fde40" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/dusk/zipball/dce7c4cc1c308bb18e95b2b3bf7d06d3f040a1f6", - "reference": "dce7c4cc1c308bb18e95b2b3bf7d06d3f040a1f6", + "url": "https://api.github.com/repos/laravel/dusk/zipball/5bff1e8dd87ec653a2202475377152e5d14fde40", + "reference": "5bff1e8dd87ec653a2202475377152e5d14fde40", "shasum": "" }, "require": { "ext-json": "*", "ext-zip": "*", - "guzzlehttp/guzzle": "^7.2", - "illuminate/console": "^9.0|^10.0", - "illuminate/support": "^9.0|^10.0", - "nesbot/carbon": "^2.0", - "php": "^8.0", + "guzzlehttp/guzzle": "^7.5", + "illuminate/console": "^10.0|^11.0", + "illuminate/support": "^10.0|^11.0", + "php": "^8.1", "php-webdriver/webdriver": "^1.9.0", - "symfony/console": "^6.0", - "symfony/finder": "^6.0", - "symfony/process": "^6.0", + "symfony/console": "^6.2|^7.0", + "symfony/finder": "^6.2|^7.0", + "symfony/process": "^6.2|^7.0", "vlucas/phpdotenv": "^5.2" }, "require-dev": { - "mockery/mockery": "^1.4.2", - "orchestra/testbench": "^7.33|^8.13", + "mockery/mockery": "^1.6", + "orchestra/testbench": "^8.19|^9.0", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.5.10|^10.0.1", - "psy/psysh": "^0.11.12" + "phpunit/phpunit": "^10.1|^11.0", + "psy/psysh": "^0.11.12|^0.12" }, "suggest": { "ext-pcntl": "Used to gracefully terminate Dusk when tests are running." }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "7.x-dev" - }, "laravel": { "providers": [ "Laravel\\Dusk\\DuskServiceProvider" @@ -12701,22 +12328,22 @@ ], "support": { "issues": "https://github.com/laravel/dusk/issues", - "source": "https://github.com/laravel/dusk/tree/v7.13.0" + "source": "https://github.com/laravel/dusk/tree/v8.2.8" }, - "time": "2024-02-23T22:29:53+00:00" + "time": "2024-10-04T14:02:20+00:00" }, { "name": "laravel/pint", - "version": "v1.16.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "1b3a3dc5bc6a81ff52828ba7277621f1d49d6d98" + "reference": "35c00c05ec43e6b46d295efc0f4386ceb30d50d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/1b3a3dc5bc6a81ff52828ba7277621f1d49d6d98", - "reference": "1b3a3dc5bc6a81ff52828ba7277621f1d49d6d98", + "url": "https://api.github.com/repos/laravel/pint/zipball/35c00c05ec43e6b46d295efc0f4386ceb30d50d9", + "reference": "35c00c05ec43e6b46d295efc0f4386ceb30d50d9", "shasum": "" }, "require": { @@ -12727,13 +12354,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.57.1", - "illuminate/view": "^10.48.10", - "larastan/larastan": "^2.9.6", + "friendsofphp/php-cs-fixer": "^3.64.0", + "illuminate/view": "^10.48.20", + "larastan/larastan": "^2.9.8", "laravel-zero/framework": "^10.4.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.34.7" + "pestphp/pest": "^2.35.1" }, "bin": [ "builds/pint" @@ -12769,7 +12396,144 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-05-21T18:08:25+00:00" + "time": "2024-09-24T17:22:50+00:00" + }, + { + "name": "laravel/telescope", + "version": "v5.2.4", + "source": { + "type": "git", + "url": "https://github.com/laravel/telescope.git", + "reference": "749369e996611d803e7c1b57929b482dd676008d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/telescope/zipball/749369e996611d803e7c1b57929b482dd676008d", + "reference": "749369e996611d803e7c1b57929b482dd676008d", + "shasum": "" + }, + "require": { + "ext-json": "*", + "laravel/framework": "^8.37|^9.0|^10.0|^11.0", + "php": "^8.0", + "symfony/console": "^5.3|^6.0|^7.0", + "symfony/var-dumper": "^5.0|^6.0|^7.0" + }, + "require-dev": { + "ext-gd": "*", + "guzzlehttp/guzzle": "^6.0|^7.0", + "laravel/octane": "^1.4|^2.0|dev-develop", + "orchestra/testbench": "^6.40|^7.37|^8.17|^9.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.0|^10.5" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Telescope\\TelescopeServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Telescope\\": "src/", + "Laravel\\Telescope\\Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Mohamed Said", + "email": "mohamed@laravel.com" + } + ], + "description": "An elegant debug assistant for the Laravel framework.", + "keywords": [ + "debugging", + "laravel", + "monitoring" + ], + "support": { + "issues": "https://github.com/laravel/telescope/issues", + "source": "https://github.com/laravel/telescope/tree/v5.2.4" + }, + "time": "2024-10-29T15:35:13+00:00" + }, + { + "name": "maximebf/debugbar", + "version": "v1.23.2", + "source": { + "type": "git", + "url": "https://github.com/maximebf/php-debugbar.git", + "reference": "689720d724c771ac4add859056744b7b3f2406da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/689720d724c771ac4add859056744b7b3f2406da", + "reference": "689720d724c771ac4add859056744b7b3f2406da", + "shasum": "" + }, + "require": { + "php": "^7.2|^8", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^4|^5|^6|^7" + }, + "require-dev": { + "dbrekelmans/bdi": "^1", + "phpunit/phpunit": "^8|^9", + "symfony/panther": "^1|^2.1", + "twig/twig": "^1.38|^2.7|^3.0" + }, + "suggest": { + "kriswallsmith/assetic": "The best way to manage assets", + "monolog/monolog": "Log using Monolog", + "predis/predis": "Redis storage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.23-dev" + } + }, + "autoload": { + "psr-4": { + "DebugBar\\": "src/DebugBar/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maxime Bouroumeau-Fuseau", + "email": "maxime.bouroumeau@gmail.com", + "homepage": "http://maximebf.com" + }, + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "Debug bar in the browser for php application", + "homepage": "https://github.com/maximebf/php-debugbar", + "keywords": [ + "debug", + "debugbar" + ], + "support": { + "issues": "https://github.com/maximebf/php-debugbar/issues", + "source": "https://github.com/maximebf/php-debugbar/tree/v1.23.2" + }, + "time": "2024-09-16T11:23:09+00:00" }, { "name": "mockery/mockery", @@ -12856,16 +12620,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", "shasum": "" }, "require": { @@ -12873,11 +12637,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", @@ -12903,7 +12668,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" }, "funding": [ { @@ -12911,44 +12676,42 @@ "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2024-06-12T14:39:25+00:00" }, { "name": "nunomaduro/collision", - "version": "v7.10.0", + "version": "v8.4.0", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "49ec67fa7b002712da8526678abd651c09f375b2" + "reference": "e7d1aa8ed753f63fa816932bbc89678238843b4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/49ec67fa7b002712da8526678abd651c09f375b2", - "reference": "49ec67fa7b002712da8526678abd651c09f375b2", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/e7d1aa8ed753f63fa816932bbc89678238843b4a", + "reference": "e7d1aa8ed753f63fa816932bbc89678238843b4a", "shasum": "" }, "require": { - "filp/whoops": "^2.15.3", - "nunomaduro/termwind": "^1.15.1", - "php": "^8.1.0", - "symfony/console": "^6.3.4" + "filp/whoops": "^2.15.4", + "nunomaduro/termwind": "^2.0.1", + "php": "^8.2.0", + "symfony/console": "^7.1.3" }, "conflict": { - "laravel/framework": ">=11.0.0" + "laravel/framework": "<11.0.0 || >=12.0.0", + "phpunit/phpunit": "<10.5.1 || >=12.0.0" }, "require-dev": { - "brianium/paratest": "^7.3.0", - "laravel/framework": "^10.28.0", - "laravel/pint": "^1.13.3", - "laravel/sail": "^1.25.0", - "laravel/sanctum": "^3.3.1", - "laravel/tinker": "^2.8.2", - "nunomaduro/larastan": "^2.6.4", - "orchestra/testbench-core": "^8.13.0", - "pestphp/pest": "^2.23.2", - "phpunit/phpunit": "^10.4.1", - "sebastian/environment": "^6.0.1", - "spatie/laravel-ignition": "^2.3.1" + "larastan/larastan": "^2.9.8", + "laravel/framework": "^11.19.0", + "laravel/pint": "^1.17.1", + "laravel/sail": "^1.31.0", + "laravel/sanctum": "^4.0.2", + "laravel/tinker": "^2.9.0", + "orchestra/testbench-core": "^9.2.3", + "pestphp/pest": "^2.35.0 || ^3.0.0", + "sebastian/environment": "^6.1.0 || ^7.0.0" }, "type": "library", "extra": { @@ -12956,6 +12719,9 @@ "providers": [ "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" } }, "autoload": { @@ -13007,25 +12773,25 @@ "type": "patreon" } ], - "time": "2023-10-11T15:45:01+00:00" + "time": "2024-08-03T15:32:23+00:00" }, { "name": "pestphp/pest", - "version": "v2.34.7", + "version": "v2.35.1", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "a7a3e4240e341d0fee1c54814ce18adc26ce5a76" + "reference": "b13acb630df52c06123588d321823c31fc685545" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/a7a3e4240e341d0fee1c54814ce18adc26ce5a76", - "reference": "a7a3e4240e341d0fee1c54814ce18adc26ce5a76", + "url": "https://api.github.com/repos/pestphp/pest/zipball/b13acb630df52c06123588d321823c31fc685545", + "reference": "b13acb630df52c06123588d321823c31fc685545", "shasum": "" }, "require": { "brianium/paratest": "^7.3.1", - "nunomaduro/collision": "^7.10.0|^8.1.1", + "nunomaduro/collision": "^7.10.0|^8.4.0", "nunomaduro/termwind": "^1.15.1|^2.0.1", "pestphp/pest-plugin": "^2.1.1", "pestphp/pest-plugin-arch": "^2.7.0", @@ -13039,8 +12805,8 @@ }, "require-dev": { "pestphp/pest-dev-tools": "^2.16.0", - "pestphp/pest-plugin-type-coverage": "^2.8.1", - "symfony/process": "^6.4.0|^7.0.4" + "pestphp/pest-plugin-type-coverage": "^2.8.5", + "symfony/process": "^6.4.0|^7.1.3" }, "bin": [ "bin/pest" @@ -13103,7 +12869,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v2.34.7" + "source": "https://github.com/pestphp/pest/tree/v2.35.1" }, "funding": [ { @@ -13115,7 +12881,7 @@ "type": "github" } ], - "time": "2024-04-05T07:44:17+00:00" + "time": "2024-08-20T21:41:50+00:00" }, { "name": "pestphp/pest-plugin", @@ -13508,32 +13274,32 @@ }, { "name": "phpunit/php-code-coverage", - "version": "10.1.14", + "version": "10.1.16", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b" + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/e3f51450ebffe8e0efdf7346ae966a656f7d5e5b", - "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.18 || ^5.0", + "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=8.1", - "phpunit/php-file-iterator": "^4.0", - "phpunit/php-text-template": "^3.0", - "sebastian/code-unit-reverse-lookup": "^3.0", - "sebastian/complexity": "^3.0", - "sebastian/environment": "^6.0", - "sebastian/lines-of-code": "^2.0", - "sebastian/version": "^4.0", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { "phpunit/phpunit": "^10.1" @@ -13545,7 +13311,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "10.1-dev" + "dev-main": "10.1.x-dev" } }, "autoload": { @@ -13574,7 +13340,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.14" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" }, "funding": [ { @@ -13582,7 +13348,7 @@ "type": "github" } ], - "time": "2024-03-12T15:33:41+00:00" + "time": "2024-08-22T04:31:57+00:00" }, { "name": "phpunit/php-file-iterator", @@ -14098,16 +13864,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.1", + "version": "5.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "2db5010a484d53ebf536087a70b4a5423c102372" + "reference": "2d3e04c3b4c1e84a5e7382221ad8883c8fbc4f53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2db5010a484d53ebf536087a70b4a5423c102372", - "reference": "2db5010a484d53ebf536087a70b4a5423c102372", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2d3e04c3b4c1e84a5e7382221ad8883c8fbc4f53", + "reference": "2d3e04c3b4c1e84a5e7382221ad8883c8fbc4f53", "shasum": "" }, "require": { @@ -14118,7 +13884,7 @@ "sebastian/exporter": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^10.3" + "phpunit/phpunit": "^10.4" }, "type": "library", "extra": { @@ -14163,7 +13929,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.1" + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.2" }, "funding": [ { @@ -14171,7 +13937,7 @@ "type": "github" } ], - "time": "2023-08-14T13:18:12+00:00" + "time": "2024-08-12T06:03:08+00:00" }, { "name": "sebastian/complexity", @@ -14890,23 +14656,97 @@ "time": "2022-05-20T15:13:10+00:00" }, { - "name": "spatie/flare-client-php", - "version": "1.6.0", + "name": "spatie/error-solutions", + "version": "1.1.1", "source": { "type": "git", - "url": "https://github.com/spatie/flare-client-php.git", - "reference": "220a7c8745e9fa427d54099f47147c4b97fe6462" + "url": "https://github.com/spatie/error-solutions.git", + "reference": "ae7393122eda72eed7cc4f176d1e96ea444f2d67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/flare-client-php/zipball/220a7c8745e9fa427d54099f47147c4b97fe6462", - "reference": "220a7c8745e9fa427d54099f47147c4b97fe6462", + "url": "https://api.github.com/repos/spatie/error-solutions/zipball/ae7393122eda72eed7cc4f176d1e96ea444f2d67", + "reference": "ae7393122eda72eed7cc4f176d1e96ea444f2d67", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "illuminate/broadcasting": "^10.0|^11.0", + "illuminate/cache": "^10.0|^11.0", + "illuminate/support": "^10.0|^11.0", + "livewire/livewire": "^2.11|^3.3.5", + "openai-php/client": "^0.10.1", + "orchestra/testbench": "^7.0|8.22.3|^9.0", + "pestphp/pest": "^2.20", + "phpstan/phpstan": "^1.11", + "psr/simple-cache": "^3.0", + "psr/simple-cache-implementation": "^3.0", + "spatie/ray": "^1.28", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "vlucas/phpdotenv": "^5.5" + }, + "suggest": { + "openai-php/client": "Require get solutions from OpenAI", + "simple-cache-implementation": "To cache solutions from OpenAI" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Ignition\\": "legacy/ignition", + "Spatie\\ErrorSolutions\\": "src", + "Spatie\\LaravelIgnition\\": "legacy/laravel-ignition" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "role": "Developer" + } + ], + "description": "This is my package error-solutions", + "homepage": "https://github.com/spatie/error-solutions", + "keywords": [ + "error-solutions", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/error-solutions/issues", + "source": "https://github.com/spatie/error-solutions/tree/1.1.1" + }, + "funding": [ + { + "url": "https://github.com/Spatie", + "type": "github" + } + ], + "time": "2024-07-25T11:06:04+00:00" + }, + { + "name": "spatie/flare-client-php", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/flare-client-php.git", + "reference": "180f8ca4c0d0d6fc51477bd8c53ce37ab5a96122" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/flare-client-php/zipball/180f8ca4c0d0d6fc51477bd8c53ce37ab5a96122", + "reference": "180f8ca4c0d0d6fc51477bd8c53ce37ab5a96122", "shasum": "" }, "require": { "illuminate/pipeline": "^8.0|^9.0|^10.0|^11.0", "php": "^8.0", - "spatie/backtrace": "^1.5.2", + "spatie/backtrace": "^1.6.1", "symfony/http-foundation": "^5.2|^6.0|^7.0", "symfony/mime": "^5.2|^6.0|^7.0", "symfony/process": "^5.2|^6.0|^7.0", @@ -14918,7 +14758,7 @@ "phpstan/extension-installer": "^1.1", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-phpunit": "^1.0", - "spatie/phpunit-snapshot-assertions": "^4.0|^5.0" + "spatie/pest-plugin-snapshots": "^1.0|^2.0" }, "type": "library", "extra": { @@ -14948,7 +14788,7 @@ ], "support": { "issues": "https://github.com/spatie/flare-client-php/issues", - "source": "https://github.com/spatie/flare-client-php/tree/1.6.0" + "source": "https://github.com/spatie/flare-client-php/tree/1.8.0" }, "funding": [ { @@ -14956,28 +14796,28 @@ "type": "github" } ], - "time": "2024-05-22T09:45:39+00:00" + "time": "2024-08-01T08:27:26+00:00" }, { "name": "spatie/ignition", - "version": "1.14.1", + "version": "1.15.0", "source": { "type": "git", "url": "https://github.com/spatie/ignition.git", - "reference": "c23cc018c5f423d2f413b99f84655fceb6549811" + "reference": "e3a68e137371e1eb9edc7f78ffa733f3b98991d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/ignition/zipball/c23cc018c5f423d2f413b99f84655fceb6549811", - "reference": "c23cc018c5f423d2f413b99f84655fceb6549811", + "url": "https://api.github.com/repos/spatie/ignition/zipball/e3a68e137371e1eb9edc7f78ffa733f3b98991d2", + "reference": "e3a68e137371e1eb9edc7f78ffa733f3b98991d2", "shasum": "" }, "require": { "ext-json": "*", "ext-mbstring": "*", "php": "^8.0", - "spatie/backtrace": "^1.5.3", - "spatie/flare-client-php": "^1.4.0", + "spatie/error-solutions": "^1.0", + "spatie/flare-client-php": "^1.7", "symfony/console": "^5.4|^6.0|^7.0", "symfony/var-dumper": "^5.4|^6.0|^7.0" }, @@ -15039,20 +14879,20 @@ "type": "github" } ], - "time": "2024-05-03T15:56:16+00:00" + "time": "2024-06-12T14:55:22+00:00" }, { "name": "spatie/laravel-ignition", - "version": "2.7.0", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-ignition.git", - "reference": "f52124d50122611e8a40f628cef5c19ff6cc5b57" + "reference": "3c067b75bfb50574db8f7e2c3978c65eed71126c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/f52124d50122611e8a40f628cef5c19ff6cc5b57", - "reference": "f52124d50122611e8a40f628cef5c19ff6cc5b57", + "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/3c067b75bfb50574db8f7e2c3978c65eed71126c", + "reference": "3c067b75bfb50574db8f7e2c3978c65eed71126c", "shasum": "" }, "require": { @@ -15061,8 +14901,7 @@ "ext-mbstring": "*", "illuminate/support": "^10.0|^11.0", "php": "^8.1", - "spatie/flare-client-php": "^1.5", - "spatie/ignition": "^1.14", + "spatie/ignition": "^1.15", "symfony/console": "^6.2.3|^7.0", "symfony/var-dumper": "^6.2.3|^7.0" }, @@ -15131,7 +14970,178 @@ "type": "github" } ], - "time": "2024-05-02T13:42:49+00:00" + "time": "2024-06-12T15:01:18+00:00" + }, + { + "name": "symfony/http-client", + "version": "v6.4.14", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "05d88cbd816ad6e0202edd9a9963cb9d615b8826" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/05d88cbd816ad6e0202edd9a9963cb9d615b8826", + "reference": "05d88cbd816ad6e0202edd9a9963cb9d615b8826", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "^3.4.1", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.3" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/amp": "^2.5", + "amphp/http-client": "^4.2.1", + "amphp/http-tunnel": "^1.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v6.4.14" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-05T16:39:55+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "20414d96f391677bf80078aa55baece78b82647d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", + "reference": "20414d96f391677bf80078aa55baece78b82647d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:32:20+00:00" }, { "name": "ta-tikoma/phpunit-architecture-test", diff --git a/config/app.php b/config/app.php index 41e94c09b..371ac44ec 100644 --- a/config/app.php +++ b/config/app.php @@ -199,7 +199,6 @@ return [ App\Providers\EventServiceProvider::class, App\Providers\HorizonServiceProvider::class, App\Providers\RouteServiceProvider::class, - ], /* diff --git a/config/constants.php b/config/constants.php index 444d144a8..1bec2e3bf 100644 --- a/config/constants.php +++ b/config/constants.php @@ -1,12 +1,14 @@ '26.0', 'docs' => [ 'base_url' => 'https://coolify.io/docs', 'contact' => 'https://coolify.io/docs/contact', ], 'ssh' => [ - 'mux_persist_time' => env('SSH_MUX_PERSIST_TIME', '1m'), + 'mux_enabled' => env('MUX_ENABLED', env('SSH_MUX_ENABLED', true)), + 'mux_persist_time' => env('SSH_MUX_PERSIST_TIME', 3600), 'connection_timeout' => 10, 'server_interval' => 20, 'command_timeout' => 7200, @@ -17,13 +19,13 @@ return [ 'invitation' => [ 'link' => [ 'base_url' => '/invitations/', - 'expiration' => 10, + 'expiration_days' => 3, ], ], 'services' => [ // Temporary disabled until cache is implemented - 'official' => 'https://cdn.coollabs.io/coolify/service-templates.json', - // 'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/main/templates/service-templates.json', + // 'official' => 'https://cdn.coollabs.io/coolify/service-templates.json', + 'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/main/templates/service-templates.json', ], 'limits' => [ 'trial_period' => 0, diff --git a/config/coolify.php b/config/coolify.php index c7cfe6101..225dfe6fa 100644 --- a/config/coolify.php +++ b/config/coolify.php @@ -1,18 +1,17 @@ env('SENTRY_DSN'), 'docs' => 'https://coolify.io/docs/', 'contact' => 'https://coolify.io/docs/contact', 'feedback_discord_webhook' => env('FEEDBACK_DISCORD_WEBHOOK'), 'self_hosted' => env('SELF_HOSTED', true), 'waitlist' => env('WAITLIST', false), 'license_url' => 'https://licenses.coollabs.io', - 'mux_enabled' => env('MUX_ENABLED', true), 'dev_webhook' => env('SERVEO_URL'), 'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), - 'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper:latest'), + 'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper'), 'is_horizon_enabled' => env('HORIZON_ENABLED', true), 'is_scheduler_enabled' => env('SCHEDULER_ENABLED', true), - 'is_sentinel_enabled' => env('SENTINEL_ENABLED', false), ]; diff --git a/config/database.php b/config/database.php index 248c6150a..f48a68082 100644 --- a/config/database.php +++ b/config/database.php @@ -35,34 +35,6 @@ return [ 'connections' => [ - 'sqlite' => [ - 'driver' => 'sqlite', - 'url' => env('DATABASE_URL'), - 'database' => env('DB_DATABASE', database_path('database.sqlite')), - 'prefix' => '', - 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), - ], - - 'mysql' => [ - 'driver' => 'mysql', - 'url' => env('DATABASE_URL'), - 'host' => env('DB_HOST', '127.0.0.1'), - 'port' => env('DB_PORT', '3306'), - 'database' => env('DB_DATABASE', 'forge'), - 'username' => env('DB_USERNAME', 'forge'), - 'password' => env('DB_PASSWORD', ''), - 'unix_socket' => env('DB_SOCKET', ''), - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => '', - 'prefix_indexes' => true, - 'strict' => true, - 'engine' => null, - 'options' => extension_loaded('pdo_mysql') ? array_filter([ - PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), - ]) : [], - ], - 'pgsql' => [ 'driver' => 'pgsql', 'url' => env('DATABASE_URL'), @@ -77,22 +49,6 @@ return [ 'search_path' => 'public', 'sslmode' => 'prefer', ], - - 'sqlsrv' => [ - 'driver' => 'sqlsrv', - 'url' => env('DATABASE_URL'), - 'host' => env('DB_HOST', 'localhost'), - 'port' => env('DB_PORT', '1433'), - 'database' => env('DB_DATABASE', 'forge'), - 'username' => env('DB_USERNAME', 'forge'), - 'password' => env('DB_PASSWORD', ''), - 'charset' => 'utf8', - 'prefix' => '', - 'prefix_indexes' => true, - // 'encrypt' => env('DB_ENCRYPT', 'yes'), - // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), - ], - ], /* diff --git a/config/debugbar.php b/config/debugbar.php new file mode 100644 index 000000000..daeea96b6 --- /dev/null +++ b/config/debugbar.php @@ -0,0 +1,326 @@ + env('DEBUGBAR_ENABLED', null), + 'except' => [ + 'telescope*', + 'horizon*', + 'api*', + ], + + /* + |-------------------------------------------------------------------------- + | Storage settings + |-------------------------------------------------------------------------- + | + | DebugBar stores data for session/ajax requests. + | You can disable this, so the debugbar stores data in headers/session, + | but this can cause problems with large data collectors. + | By default, file storage (in the storage folder) is used. Redis and PDO + | can also be used. For PDO, run the package migrations first. + | + | Warning: Enabling storage.open will allow everyone to access previous + | request, do not enable open storage in publicly available environments! + | Specify a callback if you want to limit based on IP or authentication. + | Leaving it to null will allow localhost only. + */ + 'storage' => [ + 'enabled' => true, + 'open' => env('DEBUGBAR_OPEN_STORAGE'), // bool/callback. + 'driver' => 'file', // redis, file, pdo, socket, custom + 'path' => storage_path('debugbar'), // For file driver + 'connection' => null, // Leave null for default connection (Redis/PDO) + 'provider' => '', // Instance of StorageInterface for custom driver + 'hostname' => '127.0.0.1', // Hostname to use with the "socket" driver + 'port' => 2304, // Port to use with the "socket" driver + ], + + /* + |-------------------------------------------------------------------------- + | Editor + |-------------------------------------------------------------------------- + | + | Choose your preferred editor to use when clicking file name. + | + | Supported: "phpstorm", "vscode", "vscode-insiders", "vscode-remote", + | "vscode-insiders-remote", "vscodium", "textmate", "emacs", + | "sublime", "atom", "nova", "macvim", "idea", "netbeans", + | "xdebug", "espresso" + | + */ + + 'editor' => env('DEBUGBAR_EDITOR') ?: env('IGNITION_EDITOR', 'phpstorm'), + + /* + |-------------------------------------------------------------------------- + | Remote Path Mapping + |-------------------------------------------------------------------------- + | + | If you are using a remote dev server, like Laravel Homestead, Docker, or + | even a remote VPS, it will be necessary to specify your path mapping. + | + | Leaving one, or both of these, empty or null will not trigger the remote + | URL changes and Debugbar will treat your editor links as local files. + | + | "remote_sites_path" is an absolute base path for your sites or projects + | in Homestead, Vagrant, Docker, or another remote development server. + | + | Example value: "/home/vagrant/Code" + | + | "local_sites_path" is an absolute base path for your sites or projects + | on your local computer where your IDE or code editor is running on. + | + | Example values: "/Users//Code", "C:\Users\\Documents\Code" + | + */ + + 'remote_sites_path' => env('DEBUGBAR_REMOTE_SITES_PATH'), + 'local_sites_path' => env('DEBUGBAR_LOCAL_SITES_PATH', env('IGNITION_LOCAL_SITES_PATH')), + + /* + |-------------------------------------------------------------------------- + | Vendors + |-------------------------------------------------------------------------- + | + | Vendor files are included by default, but can be set to false. + | This can also be set to 'js' or 'css', to only include javascript or css vendor files. + | Vendor files are for css: font-awesome (including fonts) and highlight.js (css files) + | and for js: jquery and highlight.js + | So if you want syntax highlighting, set it to true. + | jQuery is set to not conflict with existing jQuery scripts. + | + */ + + 'include_vendors' => true, + + /* + |-------------------------------------------------------------------------- + | Capture Ajax Requests + |-------------------------------------------------------------------------- + | + | The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors), + | you can use this option to disable sending the data through the headers. + | + | Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools. + | + | Note for your request to be identified as ajax requests they must either send the header + | X-Requested-With with the value XMLHttpRequest (most JS libraries send this), or have application/json as a Accept header. + | + | By default `ajax_handler_auto_show` is set to true allowing ajax requests to be shown automatically in the Debugbar. + | Changing `ajax_handler_auto_show` to false will prevent the Debugbar from reloading. + */ + + 'capture_ajax' => true, + 'add_ajax_timing' => false, + 'ajax_handler_auto_show' => true, + 'ajax_handler_enable_tab' => true, + + /* + |-------------------------------------------------------------------------- + | Custom Error Handler for Deprecated warnings + |-------------------------------------------------------------------------- + | + | When enabled, the Debugbar shows deprecated warnings for Symfony components + | in the Messages tab. + | + */ + 'error_handler' => false, + + /* + |-------------------------------------------------------------------------- + | Clockwork integration + |-------------------------------------------------------------------------- + | + | The Debugbar can emulate the Clockwork headers, so you can use the Chrome + | Extension, without the server-side code. It uses Debugbar collectors instead. + | + */ + 'clockwork' => false, + + /* + |-------------------------------------------------------------------------- + | DataCollectors + |-------------------------------------------------------------------------- + | + | Enable/disable DataCollectors + | + */ + + 'collectors' => [ + 'phpinfo' => true, // Php version + 'messages' => true, // Messages + 'time' => true, // Time Datalogger + 'memory' => true, // Memory usage + 'exceptions' => true, // Exception displayer + 'log' => true, // Logs from Monolog (merged in messages if enabled) + 'db' => true, // Show database (PDO) queries and bindings + 'views' => true, // Views with their data + 'route' => true, // Current route information + 'auth' => false, // Display Laravel authentication status + 'gate' => true, // Display Laravel Gate checks + 'session' => true, // Display session data + 'symfony_request' => true, // Only one can be enabled.. + 'mail' => true, // Catch mail messages + 'laravel' => false, // Laravel version and environment + 'events' => false, // All events fired + 'default_request' => false, // Regular or special Symfony request logger + 'logs' => false, // Add the latest log messages + 'files' => false, // Show the included files + 'config' => false, // Display config settings + 'cache' => false, // Display cache events + 'models' => true, // Display models + 'livewire' => true, // Display Livewire (when available) + 'jobs' => false, // Display dispatched jobs + ], + + /* + |-------------------------------------------------------------------------- + | Extra options + |-------------------------------------------------------------------------- + | + | Configure some DataCollectors + | + */ + + 'options' => [ + 'time' => [ + 'memory_usage' => false, // Calculated by subtracting memory start and end, it may be inaccurate + ], + 'messages' => [ + 'trace' => true, // Trace the origin of the debug message + ], + 'memory' => [ + 'reset_peak' => false, // run memory_reset_peak_usage before collecting + 'with_baseline' => false, // Set boot memory usage as memory peak baseline + 'precision' => 0, // Memory rounding precision + ], + 'auth' => [ + 'show_name' => true, // Also show the users name/email in the debugbar + 'show_guards' => true, // Show the guards that are used + ], + 'db' => [ + 'with_params' => true, // Render SQL with the parameters substituted + 'backtrace' => true, // Use a backtrace to find the origin of the query in your files. + 'backtrace_exclude_paths' => [], // Paths to exclude from backtrace. (in addition to defaults) + 'timeline' => false, // Add the queries to the timeline + 'duration_background' => true, // Show shaded background on each query relative to how long it took to execute. + 'explain' => [ // Show EXPLAIN output on queries + 'enabled' => false, + 'types' => ['SELECT'], // Deprecated setting, is always only SELECT + ], + 'hints' => false, // Show hints for common mistakes + 'show_copy' => false, // Show copy button next to the query, + 'slow_threshold' => false, // Only track queries that last longer than this time in ms + 'memory_usage' => false, // Show queries memory usage + 'soft_limit' => 100, // After the soft limit, no parameters/backtrace are captured + 'hard_limit' => 500, // After the hard limit, queries are ignored + ], + 'mail' => [ + 'timeline' => false, // Add mails to the timeline + 'show_body' => true, + ], + 'views' => [ + 'timeline' => false, // Add the views to the timeline (Experimental) + 'data' => false, //true for all data, 'keys' for only names, false for no parameters. + 'group' => 50, // Group duplicate views. Pass value to auto-group, or true/false to force + 'exclude_paths' => [ // Add the paths which you don't want to appear in the views + 'vendor/filament', // Exclude Filament components by default + ], + ], + 'route' => [ + 'label' => true, // show complete route on bar + ], + 'session' => [ + 'hiddens' => [], // hides sensitive values using array paths + ], + 'symfony_request' => [ + 'hiddens' => [], // hides sensitive values using array paths, example: request_request.password + ], + 'events' => [ + 'data' => false, // collect events data, listeners + ], + 'logs' => [ + 'file' => null, + ], + 'cache' => [ + 'values' => true, // collect cache values + ], + ], + + /* + |-------------------------------------------------------------------------- + | Inject Debugbar in Response + |-------------------------------------------------------------------------- + | + | Usually, the debugbar is added just before , by listening to the + | Response after the App is done. If you disable this, you have to add them + | in your template yourself. See http://phpdebugbar.com/docs/rendering.html + | + */ + + 'inject' => true, + + /* + |-------------------------------------------------------------------------- + | DebugBar route prefix + |-------------------------------------------------------------------------- + | + | Sometimes you want to set route prefix to be used by DebugBar to load + | its resources from. Usually the need comes from misconfigured web server or + | from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97 + | + */ + 'route_prefix' => '_debugbar', + + /* + |-------------------------------------------------------------------------- + | DebugBar route middleware + |-------------------------------------------------------------------------- + | + | Additional middleware to run on the Debugbar routes + */ + 'route_middleware' => [], + + /* + |-------------------------------------------------------------------------- + | DebugBar route domain + |-------------------------------------------------------------------------- + | + | By default DebugBar route served from the same domain that request served. + | To override default domain, specify it as a non-empty value. + */ + 'route_domain' => null, + + /* + |-------------------------------------------------------------------------- + | DebugBar theme + |-------------------------------------------------------------------------- + | + | Switches between light and dark theme. If set to auto it will respect system preferences + | Possible values: auto, light, dark + */ + 'theme' => env('DEBUGBAR_THEME', 'auto'), + + /* + |-------------------------------------------------------------------------- + | Backtrace stack limit + |-------------------------------------------------------------------------- + | + | By default, the DebugBar limits the number of frames returned by the 'debug_backtrace()' function. + | If you need larger stacktraces, you can increase this number. Setting it to 0 will result in no limit. + */ + 'debug_backtrace_limit' => 50, +]; diff --git a/config/horizon.php b/config/horizon.php index ef7df3f1b..6086b30da 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -182,7 +182,7 @@ return [ 'defaults' => [ 's6' => [ 'connection' => 'redis', - 'queue' => ['default'], + 'queue' => ['high', 'default'], 'balance' => env('HORIZON_BALANCE', 'auto'), 'maxTime' => 0, 'maxJobs' => 0, @@ -197,6 +197,7 @@ return [ 'production' => [ 's6' => [ 'autoScalingStrategy' => 'size', + 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1), 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6), 'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1), 'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1), @@ -206,6 +207,7 @@ return [ 'local' => [ 's6' => [ 'autoScalingStrategy' => 'size', + 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1), 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6), 'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1), 'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1), diff --git a/config/sanctum.php b/config/sanctum.php index 529cfdc99..f1e5fc0e5 100644 --- a/config/sanctum.php +++ b/config/sanctum.php @@ -60,8 +60,9 @@ return [ */ 'middleware' => [ - 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, - 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, + 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, + 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, ], ]; diff --git a/config/sentry.php b/config/sentry.php index 33a24edfb..232070705 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -3,11 +3,12 @@ return [ // @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/ - 'dsn' => 'https://89552af6db48f4ca6a871ec0fc42964d@o1082494.ingest.us.sentry.io/4505347448045568', + 'dsn' => config('coolify.sentry_dsn'), // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.297', + 'release' => '4.0.0-beta.368', + // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/session.php b/config/session.php index c7b176a5a..44ca7ded9 100644 --- a/config/session.php +++ b/config/session.php @@ -18,7 +18,7 @@ return [ | */ - 'driver' => env('SESSION_DRIVER', 'redis'), + 'driver' => env('SESSION_DRIVER', 'database'), /* |-------------------------------------------------------------------------- diff --git a/config/subscription.php b/config/subscription.php index 07665075f..a033862d2 100644 --- a/config/subscription.php +++ b/config/subscription.php @@ -1,50 +1,12 @@ env('SUBSCRIPTION_PROVIDER', null), // stripe, paddle, lemon + 'provider' => env('SUBSCRIPTION_PROVIDER', null), // stripe + // Stripe 'stripe_api_key' => env('STRIPE_API_KEY', null), 'stripe_webhook_secret' => env('STRIPE_WEBHOOK_SECRET', null), - 'stripe_price_id_basic_monthly' => env('STRIPE_PRICE_ID_BASIC_MONTHLY', null), - 'stripe_price_id_basic_yearly' => env('STRIPE_PRICE_ID_BASIC_YEARLY', null), - 'stripe_price_id_pro_monthly' => env('STRIPE_PRICE_ID_PRO_MONTHLY', null), - 'stripe_price_id_pro_yearly' => env('STRIPE_PRICE_ID_PRO_YEARLY', null), - 'stripe_price_id_ultimate_monthly' => env('STRIPE_PRICE_ID_ULTIMATE_MONTHLY', null), - 'stripe_price_id_ultimate_yearly' => env('STRIPE_PRICE_ID_ULTIMATE_YEARLY', null), 'stripe_excluded_plans' => env('STRIPE_EXCLUDED_PLANS', null), - - 'stripe_price_id_basic_monthly_old' => env('STRIPE_PRICE_ID_BASIC_MONTHLY_OLD', null), - 'stripe_price_id_basic_yearly_old' => env('STRIPE_PRICE_ID_BASIC_YEARLY_OLD', null), - 'stripe_price_id_pro_monthly_old' => env('STRIPE_PRICE_ID_PRO_MONTHLY_OLD', null), - 'stripe_price_id_pro_yearly_old' => env('STRIPE_PRICE_ID_PRO_YEARLY_OLD', null), - 'stripe_price_id_ultimate_monthly_old' => env('STRIPE_PRICE_ID_ULTIMATE_MONTHLY_OLD', null), - 'stripe_price_id_ultimate_yearly_old' => env('STRIPE_PRICE_ID_ULTIMATE_YEARLY_OLD', null), - 'stripe_price_id_dynamic_monthly' => env('STRIPE_PRICE_ID_DYNAMIC_MONTHLY', null), 'stripe_price_id_dynamic_yearly' => env('STRIPE_PRICE_ID_DYNAMIC_YEARLY', null), - - // Paddle - 'paddle_vendor_id' => env('PADDLE_VENDOR_ID', null), - 'paddle_vendor_auth_code' => env('PADDLE_VENDOR_AUTH_CODE', null), - 'paddle_webhook_secret' => env('PADDLE_WEBHOOK_SECRET', null), - 'paddle_public_key' => env('PADDLE_PUBLIC_KEY', null), - 'paddle_price_id_basic_monthly' => env('PADDLE_PRICE_ID_BASIC_MONTHLY', null), - 'paddle_price_id_basic_yearly' => env('PADDLE_PRICE_ID_BASIC_YEARLY', null), - 'paddle_price_id_pro_monthly' => env('PADDLE_PRICE_ID_PRO_MONTHLY', null), - 'paddle_price_id_pro_yearly' => env('PADDLE_PRICE_ID_PRO_YEARLY', null), - 'paddle_price_id_ultimate_monthly' => env('PADDLE_PRICE_ID_ULTIMATE_MONTHLY', null), - 'paddle_price_id_ultimate_yearly' => env('PADDLE_PRICE_ID_ULTIMATE_YEARLY', null), - - // Lemon - 'lemon_squeezy_api_key' => env('LEMON_SQUEEZY_API_KEY', null), - 'lemon_squeezy_webhook_secret' => env('LEMON_SQUEEZY_WEBHOOK_SECRET', null), - 'lemon_squeezy_checkout_id_basic_monthly' => env('LEMON_SQUEEZY_CHECKOUT_ID_BASIC_MONTHLY', null), - 'lemon_squeezy_checkout_id_basic_yearly' => env('LEMON_SQUEEZY_CHECKOUT_ID_BASIC_YEARLY', null), - 'lemon_squeezy_checkout_id_pro_monthly' => env('LEMON_SQUEEZY_CHECKOUT_ID_PRO_MONTHLY', null), - 'lemon_squeezy_checkout_id_pro_yearly' => env('LEMON_SQUEEZY_CHECKOUT_ID_PRO_YEARLY', null), - 'lemon_squeezy_checkout_id_ultimate_monthly' => env('LEMON_SQUEEZY_CHECKOUT_ID_ULTIMATE_MONTHLY', null), - 'lemon_squeezy_checkout_id_ultimate_yearly' => env('LEMON_SQUEEZY_CHECKOUT_ID_ULTIMATE_YEARLY', null), - 'lemon_squeezy_basic_plan_ids' => env('LEMON_SQUEEZY_BASIC_PLAN_IDS', ''), - 'lemon_squeezy_pro_plan_ids' => env('LEMON_SQUEEZY_PRO_PLAN_IDS', ''), - 'lemon_squeezy_ultimate_plan_ids' => env('LEMON_SQUEEZY_ULTIMATE_PLAN_IDS', ''), ]; diff --git a/config/telescope.php b/config/telescope.php new file mode 100644 index 000000000..c940bec8a --- /dev/null +++ b/config/telescope.php @@ -0,0 +1,205 @@ + env('TELESCOPE_ENABLED', false), + + /* + |-------------------------------------------------------------------------- + | Telescope Domain + |-------------------------------------------------------------------------- + | + | This is the subdomain where Telescope will be accessible from. If the + | setting is null, Telescope will reside under the same domain as the + | application. Otherwise, this value will be used as the subdomain. + | + */ + + 'domain' => env('TELESCOPE_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | Telescope Path + |-------------------------------------------------------------------------- + | + | This is the URI path where Telescope will be accessible from. Feel free + | to change this path to anything you like. Note that the URI will not + | affect the paths of its internal API that aren't exposed to users. + | + */ + + 'path' => env('TELESCOPE_PATH', 'telescope'), + + /* + |-------------------------------------------------------------------------- + | Telescope Storage Driver + |-------------------------------------------------------------------------- + | + | This configuration options determines the storage driver that will + | be used to store Telescope's data. In addition, you may set any + | custom options as needed by the particular driver you choose. + | + */ + + 'driver' => env('TELESCOPE_DRIVER', 'database'), + + 'storage' => [ + 'database' => [ + 'connection' => env('DB_CONNECTION', 'pgsql'), + 'chunk' => 1000, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Telescope Queue + |-------------------------------------------------------------------------- + | + | This configuration options determines the queue connection and queue + | which will be used to process ProcessPendingUpdate jobs. This can + | be changed if you would prefer to use a non-default connection. + | + */ + + 'queue' => [ + 'connection' => env('TELESCOPE_QUEUE_CONNECTION', 'redis'), + 'queue' => env('TELESCOPE_QUEUE', 'default'), + ], + + /* + |-------------------------------------------------------------------------- + | Telescope Route Middleware + |-------------------------------------------------------------------------- + | + | These middleware will be assigned to every Telescope route, giving you + | the chance to add your own middleware to this list or change any of + | the existing middleware. Or, you can simply stick with this list. + | + */ + + 'middleware' => [ + 'web', + Authorize::class, + ], + + /* + |-------------------------------------------------------------------------- + | Allowed / Ignored Paths & Commands + |-------------------------------------------------------------------------- + | + | The following array lists the URI paths and Artisan commands that will + | not be watched by Telescope. In addition to this list, some Laravel + | commands, like migrations and queue commands, are always ignored. + | + */ + + 'only_paths' => [ + // 'api/*' + ], + + 'ignore_paths' => [ + 'livewire*', + 'nova-api*', + 'pulse*', + ], + + 'ignore_commands' => [ + // + ], + + /* + |-------------------------------------------------------------------------- + | Telescope Watchers + |-------------------------------------------------------------------------- + | + | The following array lists the "watchers" that will be registered with + | Telescope. The watchers gather the application's profile data when + | a request or task is executed. Feel free to customize this list. + | + */ + + 'watchers' => [ + Watchers\BatchWatcher::class => env('TELESCOPE_BATCH_WATCHER', true), + + Watchers\CacheWatcher::class => [ + 'enabled' => env('TELESCOPE_CACHE_WATCHER', true), + 'hidden' => [], + ], + + Watchers\ClientRequestWatcher::class => env('TELESCOPE_CLIENT_REQUEST_WATCHER', true), + + Watchers\CommandWatcher::class => [ + 'enabled' => env('TELESCOPE_COMMAND_WATCHER', true), + 'ignore' => [], + ], + + Watchers\DumpWatcher::class => [ + 'enabled' => env('TELESCOPE_DUMP_WATCHER', true), + 'always' => env('TELESCOPE_DUMP_WATCHER_ALWAYS', false), + ], + + Watchers\EventWatcher::class => [ + 'enabled' => env('TELESCOPE_EVENT_WATCHER', true), + 'ignore' => [], + ], + + Watchers\ExceptionWatcher::class => env('TELESCOPE_EXCEPTION_WATCHER', true), + + Watchers\GateWatcher::class => [ + 'enabled' => env('TELESCOPE_GATE_WATCHER', true), + 'ignore_abilities' => [], + 'ignore_packages' => true, + 'ignore_paths' => [], + ], + + Watchers\JobWatcher::class => env('TELESCOPE_JOB_WATCHER', true), + + Watchers\LogWatcher::class => [ + 'enabled' => env('TELESCOPE_LOG_WATCHER', true), + 'level' => 'error', + ], + + Watchers\MailWatcher::class => env('TELESCOPE_MAIL_WATCHER', true), + + Watchers\ModelWatcher::class => [ + 'enabled' => env('TELESCOPE_MODEL_WATCHER', true), + 'events' => ['eloquent.*'], + 'hydrations' => true, + ], + + Watchers\NotificationWatcher::class => env('TELESCOPE_NOTIFICATION_WATCHER', true), + + Watchers\QueryWatcher::class => [ + 'enabled' => env('TELESCOPE_QUERY_WATCHER', true), + 'ignore_packages' => true, + 'ignore_paths' => [], + 'slow' => 100, + ], + + Watchers\RedisWatcher::class => env('TELESCOPE_REDIS_WATCHER', true), + + Watchers\RequestWatcher::class => [ + 'enabled' => env('TELESCOPE_REQUEST_WATCHER', true), + 'size_limit' => env('TELESCOPE_RESPONSE_SIZE_LIMIT', 64), + 'ignore_http_methods' => [], + 'ignore_status_codes' => [], + ], + + Watchers\ScheduleWatcher::class => env('TELESCOPE_SCHEDULE_WATCHER', true), + Watchers\ViewWatcher::class => env('TELESCOPE_VIEW_WATCHER', true), + ], +]; diff --git a/config/testing.php b/config/testing.php new file mode 100644 index 000000000..41b8eadf0 --- /dev/null +++ b/config/testing.php @@ -0,0 +1,6 @@ + env('DUSK_TEST_EMAIL', 'test@example.com'), + 'dusk_test_password' => env('DUSK_TEST_PASSWORD', 'password'), +]; diff --git a/config/version.php b/config/version.php index 06c1e6c66..ddb1cb354 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ getConnection()); + + $schema->create('telescope_entries', function (Blueprint $table) { + $table->bigIncrements('sequence'); + $table->uuid('uuid'); + $table->uuid('batch_id'); + $table->string('family_hash')->nullable(); + $table->boolean('should_display_on_index')->default(true); + $table->string('type', 20); + $table->longText('content'); + $table->dateTime('created_at')->nullable(); + + $table->unique('uuid'); + $table->index('batch_id'); + $table->index('family_hash'); + $table->index('created_at'); + $table->index(['type', 'should_display_on_index']); + }); + + $schema->create('telescope_entries_tags', function (Blueprint $table) { + $table->uuid('entry_uuid'); + $table->string('tag'); + + $table->primary(['entry_uuid', 'tag']); + $table->index('tag'); + + $table->foreign('entry_uuid') + ->references('uuid') + ->on('telescope_entries') + ->onDelete('cascade'); + }); + + $schema->create('telescope_monitoring', function (Blueprint $table) { + $table->string('tag')->primary(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $schema = Schema::connection($this->getConnection()); + + $schema->dropIfExists('telescope_entries_tags'); + $schema->dropIfExists('telescope_entries'); + $schema->dropIfExists('telescope_monitoring'); + } +}; diff --git a/database/migrations/2023_07_27_182013_smtp_discord_schemaless_to_normal.php b/database/migrations/2023_07_27_182013_smtp_discord_schemaless_to_normal.php index 8bb8d7824..3e3a90b6d 100644 --- a/database/migrations/2023_07_27_182013_smtp_discord_schemaless_to_normal.php +++ b/database/migrations/2023_07_27_182013_smtp_discord_schemaless_to_normal.php @@ -163,7 +163,7 @@ return new class extends Migration $table->schemalessAttributes('smtp'); }); - $instance_setting = InstanceSettings::get(); + $instance_setting = instanceSettings(); $instance_setting->smtp = [ 'enabled' => $instance_setting->smtp_enabled, 'from_address' => $instance_setting->smtp_from_address, diff --git a/database/migrations/2023_08_22_071051_add_stripe_plan_to_subscriptions.php b/database/migrations/2023_08_22_071051_add_stripe_plan_to_subscriptions.php index 3da187320..a04eaf983 100644 --- a/database/migrations/2023_08_22_071051_add_stripe_plan_to_subscriptions.php +++ b/database/migrations/2023_08_22_071051_add_stripe_plan_to_subscriptions.php @@ -13,7 +13,6 @@ return new class extends Migration { Schema::table('subscriptions', function (Blueprint $table) { $table->string('stripe_plan_id')->nullable()->after('stripe_cancel_at_period_end'); - }); } diff --git a/database/migrations/2023_08_22_071054_add_stripe_reasons.php b/database/migrations/2023_08_22_071054_add_stripe_reasons.php index efd611aac..6ffe37e98 100644 --- a/database/migrations/2023_08_22_071054_add_stripe_reasons.php +++ b/database/migrations/2023_08_22_071054_add_stripe_reasons.php @@ -14,7 +14,6 @@ return new class extends Migration Schema::table('subscriptions', function (Blueprint $table) { $table->string('stripe_feedback')->nullable()->after('stripe_cancel_at_period_end'); $table->string('stripe_comment')->nullable()->after('stripe_feedback'); - }); } diff --git a/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php b/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php index c22317e6b..61fcbda6b 100644 --- a/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php +++ b/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php @@ -13,7 +13,6 @@ return new class extends Migration { Schema::table('subscriptions', function (Blueprint $table) { $table->boolean('stripe_trial_already_ended')->default(false)->after('stripe_cancel_at_period_end'); - }); } diff --git a/database/migrations/2023_08_22_071060_change_invitation_link_length.php b/database/migrations/2023_08_22_071060_change_invitation_link_length.php index 4efb03351..9d14c3f26 100644 --- a/database/migrations/2023_08_22_071060_change_invitation_link_length.php +++ b/database/migrations/2023_08_22_071060_change_invitation_link_length.php @@ -13,7 +13,6 @@ return new class extends Migration { Schema::table('team_invitations', function (Blueprint $table) { $table->text('link')->change(); - }); } diff --git a/database/migrations/2023_09_20_082541_update_services_table.php b/database/migrations/2023_09_20_082541_update_services_table.php index 8c6b350f7..c70cd28f7 100644 --- a/database/migrations/2023_09_20_082541_update_services_table.php +++ b/database/migrations/2023_09_20_082541_update_services_table.php @@ -16,7 +16,6 @@ return new class extends Migration $table->longText('description')->nullable(); $table->longText('docker_compose_raw'); $table->longText('docker_compose')->nullable(); - }); } diff --git a/database/migrations/2023_09_20_083549_update_environment_variables_table.php b/database/migrations/2023_09_20_083549_update_environment_variables_table.php index 40eb6aa44..a96d096bb 100644 --- a/database/migrations/2023_09_20_083549_update_environment_variables_table.php +++ b/database/migrations/2023_09_20_083549_update_environment_variables_table.php @@ -13,7 +13,6 @@ return new class extends Migration { Schema::table('environment_variables', function (Blueprint $table) { $table->foreignId('service_id')->nullable(); - }); } diff --git a/database/migrations/2023_09_23_111809_remove_destination_from_services_table.php b/database/migrations/2023_09_23_111809_remove_destination_from_services_table.php index 920f44a72..729146a4a 100644 --- a/database/migrations/2023_09_23_111809_remove_destination_from_services_table.php +++ b/database/migrations/2023_09_23_111809_remove_destination_from_services_table.php @@ -14,7 +14,6 @@ return new class extends Migration Schema::table('services', function (Blueprint $table) { $table->dropColumn('destination_type'); $table->dropColumn('destination_id'); - }); } diff --git a/database/migrations/2023_09_23_111819_add_server_emails.php b/database/migrations/2023_09_23_111819_add_server_emails.php index 03c1e6bd2..775e82010 100644 --- a/database/migrations/2023_09_23_111819_add_server_emails.php +++ b/database/migrations/2023_09_23_111819_add_server_emails.php @@ -26,6 +26,5 @@ return new class extends Migration $table->dropColumn('unreachable_email_sent'); $table->integer('unreachable_count')->default(0); }); - } }; diff --git a/database/migrations/2023_11_16_220647_add_log_drains.php b/database/migrations/2023_11_16_220647_add_log_drains.php index 05b1ed054..f5161b3d7 100644 --- a/database/migrations/2023_11_16_220647_add_log_drains.php +++ b/database/migrations/2023_11_16_220647_add_log_drains.php @@ -22,7 +22,6 @@ return new class extends Migration $table->boolean('is_logdrain_axiom_enabled')->default(false); $table->string('logdrain_axiom_dataset_name')->nullable(); $table->string('logdrain_axiom_api_key')->nullable(); - }); } diff --git a/database/migrations/2023_12_13_110214_add_soft_deletes.php b/database/migrations/2023_12_13_110214_add_soft_deletes.php index ab7b562b4..72350b77f 100644 --- a/database/migrations/2023_12_13_110214_add_soft_deletes.php +++ b/database/migrations/2023_12_13_110214_add_soft_deletes.php @@ -66,6 +66,5 @@ return new class extends Migration Schema::table('service_databases', function (Blueprint $table) { $table->dropSoftDeletes(); }); - } }; diff --git a/database/migrations/2023_12_17_155616_add_custom_docker_compose_start_command.php b/database/migrations/2023_12_17_155616_add_custom_docker_compose_start_command.php index eeb2769fe..f28b2670e 100644 --- a/database/migrations/2023_12_17_155616_add_custom_docker_compose_start_command.php +++ b/database/migrations/2023_12_17_155616_add_custom_docker_compose_start_command.php @@ -14,7 +14,6 @@ return new class extends Migration Schema::table('applications', function (Blueprint $table) { $table->string('docker_compose_custom_start_command')->nullable(); $table->string('docker_compose_custom_build_command')->nullable(); - }); } diff --git a/database/migrations/2024_04_09_095517_make_custom_docker_commands_longer.php b/database/migrations/2024_04_09_095517_make_custom_docker_commands_longer.php index 7df53ec06..b3f2c1920 100644 --- a/database/migrations/2024_04_09_095517_make_custom_docker_commands_longer.php +++ b/database/migrations/2024_04_09_095517_make_custom_docker_commands_longer.php @@ -13,7 +13,6 @@ return new class extends Migration { Schema::table('applications', function (Blueprint $table) { $table->text('custom_docker_run_options')->nullable()->change(); - }); } diff --git a/database/migrations/2024_04_25_073615_add_docker_network_to_application_settings.php b/database/migrations/2024_04_25_073615_add_docker_network_to_application_settings.php index aeae6f77d..bea410eab 100644 --- a/database/migrations/2024_04_25_073615_add_docker_network_to_application_settings.php +++ b/database/migrations/2024_04_25_073615_add_docker_network_to_application_settings.php @@ -13,7 +13,6 @@ return new class extends Migration { Schema::table('application_settings', function (Blueprint $table) { $table->boolean('connect_to_docker_network')->default(false); - }); } diff --git a/database/migrations/2024_05_23_091713_add_gitea_webhook_to_applications.php b/database/migrations/2024_05_23_091713_add_gitea_webhook_to_applications.php index 716f1f44c..b46a07203 100644 --- a/database/migrations/2024_05_23_091713_add_gitea_webhook_to_applications.php +++ b/database/migrations/2024_05_23_091713_add_gitea_webhook_to_applications.php @@ -13,7 +13,6 @@ return new class extends Migration { Schema::table('applications', function (Blueprint $table) { $table->string('manual_webhook_secret_gitea')->nullable(); - }); } diff --git a/database/migrations/2024_06_18_105948_move_server_metrics.php b/database/migrations/2024_06_18_105948_move_server_metrics.php new file mode 100644 index 000000000..a6bccd16a --- /dev/null +++ b/database/migrations/2024_06_18_105948_move_server_metrics.php @@ -0,0 +1,40 @@ +dropColumn('is_metrics_enabled'); + }); + Schema::table('server_settings', function (Blueprint $table) { + $table->boolean('is_metrics_enabled')->default(false); + $table->integer('metrics_refresh_rate_seconds')->default(5); + $table->integer('metrics_history_days')->default(30); + $table->string('metrics_token')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->boolean('is_metrics_enabled')->default(true); + }); + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('is_metrics_enabled'); + $table->dropColumn('metrics_refresh_rate_seconds'); + $table->dropColumn('metrics_history_days'); + $table->dropColumn('metrics_token'); + }); + } +}; diff --git a/database/migrations/2024_06_20_102551_add_server_api_sentinel.php b/database/migrations/2024_06_20_102551_add_server_api_sentinel.php new file mode 100644 index 000000000..b840195af --- /dev/null +++ b/database/migrations/2024_06_20_102551_add_server_api_sentinel.php @@ -0,0 +1,28 @@ +boolean('is_server_api_enabled')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('is_server_api_enabled'); + }); + } +}; diff --git a/database/migrations/2024_06_21_143358_add_api_deployment_type.php b/database/migrations/2024_06_21_143358_add_api_deployment_type.php new file mode 100644 index 000000000..03f4d4796 --- /dev/null +++ b/database/migrations/2024_06_21_143358_add_api_deployment_type.php @@ -0,0 +1,28 @@ +boolean('is_api')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_deployment_queues', function (Blueprint $table) { + $table->dropColumn('is_api'); + }); + } +}; diff --git a/database/migrations/2024_06_22_081140_alter_instance_settings_add_instance_name.php b/database/migrations/2024_06_22_081140_alter_instance_settings_add_instance_name.php new file mode 100644 index 000000000..1687e047c --- /dev/null +++ b/database/migrations/2024_06_22_081140_alter_instance_settings_add_instance_name.php @@ -0,0 +1,28 @@ +string('instance_name')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('instance_settings', function (Blueprint $table) { + $table->dropColumn('instance_name'); + }); + } +}; diff --git a/database/migrations/2024_06_25_184323_update_db.php b/database/migrations/2024_06_25_184323_update_db.php new file mode 100644 index 000000000..d9cddb15f --- /dev/null +++ b/database/migrations/2024_06_25_184323_update_db.php @@ -0,0 +1,95 @@ +dropColumn('docker_compose_pr_location'); + $table->dropColumn('docker_compose_pr'); + $table->dropColumn('docker_compose_pr_raw'); + }); + Schema::table('subscriptions', function (Blueprint $table) { + $table->dropColumn('lemon_subscription_id'); + $table->dropColumn('lemon_order_id'); + $table->dropColumn('lemon_product_id'); + $table->dropColumn('lemon_variant_id'); + $table->dropColumn('lemon_variant_name'); + $table->dropColumn('lemon_customer_id'); + $table->dropColumn('lemon_status'); + $table->dropColumn('lemon_renews_at'); + $table->dropColumn('lemon_update_payment_menthod_url'); + $table->dropColumn('lemon_trial_ends_at'); + $table->dropColumn('lemon_ends_at'); + }); + Schema::table('environment_variables', function (Blueprint $table) { + $table->string('uuid')->nullable()->after('id'); + }); + + EnvironmentVariable::all()->each(function (EnvironmentVariable $environmentVariable) { + $environmentVariable->update([ + 'uuid' => (string) new Cuid2, + ]); + }); + Schema::table('environment_variables', function (Blueprint $table) { + $table->string('uuid')->nullable(false)->change(); + }); + Schema::table('server_settings', function (Blueprint $table) { + $table->integer('metrics_history_days')->default(7)->change(); + }); + + DB::table('server_settings')->update(['metrics_history_days' => 7]); + } catch (\Exception $e) { + Log::error('Error updating db: '.$e->getMessage()); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->string('docker_compose_pr_location')->nullable()->default('/docker-compose.yaml')->after('docker_compose_location'); + $table->longText('docker_compose_pr')->nullable()->after('docker_compose_location'); + $table->longText('docker_compose_pr_raw')->nullable()->after('docker_compose'); + }); + Schema::table('subscriptions', function (Blueprint $table) { + $table->string('lemon_subscription_id')->nullable()->after('stripe_subscription_id'); + $table->string('lemon_order_id')->nullable()->after('lemon_subscription_id'); + $table->string('lemon_product_id')->nullable()->after('lemon_order_id'); + $table->string('lemon_variant_id')->nullable()->after('lemon_product_id'); + $table->string('lemon_variant_name')->nullable()->after('lemon_variant_id'); + $table->string('lemon_customer_id')->nullable()->after('lemon_variant_name'); + $table->string('lemon_status')->nullable()->after('lemon_customer_id'); + $table->timestamp('lemon_renews_at')->nullable()->after('lemon_status'); + $table->string('lemon_update_payment_menthod_url')->nullable()->after('lemon_renews_at'); + $table->timestamp('lemon_trial_ends_at')->nullable()->after('lemon_update_payment_menthod_url'); + $table->timestamp('lemon_ends_at')->nullable()->after('lemon_trial_ends_at'); + }); + Schema::table('environment_variables', function (Blueprint $table) { + $table->dropColumn('uuid'); + }); + Schema::table('server_settings', function (Blueprint $table) { + $table->integer('metrics_history_days')->default(30)->change(); + }); + Server::all()->each(function (Server $server) { + $server->settings->update([ + 'metrics_history_days' => 30, + ]); + }); + } +}; diff --git a/database/migrations/2024_07_01_115528_add_is_api_allowed_and_iplist.php b/database/migrations/2024_07_01_115528_add_is_api_allowed_and_iplist.php new file mode 100644 index 000000000..b319adb70 --- /dev/null +++ b/database/migrations/2024_07_01_115528_add_is_api_allowed_and_iplist.php @@ -0,0 +1,24 @@ +boolean('is_api_enabled')->default(true); + $table->text('allowed_ips')->nullable(); + }); + } + + public function down(): void + { + Schema::table('instance_settings', function (Blueprint $table) { + $table->dropColumn('is_api_enabled'); + $table->dropColumn('allowed_ips'); + }); + } +}; diff --git a/database/migrations/2024_07_05_120217_remove_unique_from_tag_names.php b/database/migrations/2024_07_05_120217_remove_unique_from_tag_names.php new file mode 100644 index 000000000..301de814b --- /dev/null +++ b/database/migrations/2024_07_05_120217_remove_unique_from_tag_names.php @@ -0,0 +1,28 @@ +dropUnique(['name']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('tags', function (Blueprint $table) { + $table->unique(['name']); + }); + } +}; diff --git a/database/migrations/2024_07_11_083719_application_compose_versions.php b/database/migrations/2024_07_11_083719_application_compose_versions.php new file mode 100644 index 000000000..9cdbb98d7 --- /dev/null +++ b/database/migrations/2024_07_11_083719_application_compose_versions.php @@ -0,0 +1,28 @@ +string('compose_parsing_version')->default('1'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('compose_parsing_version'); + }); + } +}; diff --git a/database/migrations/2024_07_17_123828_add_is_container_labels_readonly.php b/database/migrations/2024_07_17_123828_add_is_container_labels_readonly.php new file mode 100644 index 000000000..eb36946a4 --- /dev/null +++ b/database/migrations/2024_07_17_123828_add_is_container_labels_readonly.php @@ -0,0 +1,28 @@ +boolean('is_container_label_readonly_enabled')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_settings', function (Blueprint $table) { + $table->dropColumn('is_container_label_readonly_enabled'); + }); + } +}; diff --git a/database/migrations/2024_07_18_110424_create_application_settings_is_preserve_repository_enabled.php b/database/migrations/2024_07_18_110424_create_application_settings_is_preserve_repository_enabled.php new file mode 100644 index 000000000..25dd0ef16 --- /dev/null +++ b/database/migrations/2024_07_18_110424_create_application_settings_is_preserve_repository_enabled.php @@ -0,0 +1,28 @@ +boolean('is_preserve_repository_enabled')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_settings', function (Blueprint $table) { + $table->dropColumn('is_preserve_repository_enabled'); + }); + } +}; diff --git a/database/migrations/2024_07_18_123458_add_force_cleanup_server.php b/database/migrations/2024_07_18_123458_add_force_cleanup_server.php new file mode 100644 index 000000000..ea3695b3f --- /dev/null +++ b/database/migrations/2024_07_18_123458_add_force_cleanup_server.php @@ -0,0 +1,28 @@ +boolean('is_force_cleanup_enabled')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('is_force_cleanup_enabled'); + }); + } +}; diff --git a/database/migrations/2024_07_19_132617_disable_healtcheck_by_default.php b/database/migrations/2024_07_19_132617_disable_healtcheck_by_default.php new file mode 100644 index 000000000..5de50de0c --- /dev/null +++ b/database/migrations/2024_07_19_132617_disable_healtcheck_by_default.php @@ -0,0 +1,28 @@ +boolean('health_check_enabled')->default(false)->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->boolean('health_check_enabled')->default(true)->change(); + }); + } +}; diff --git a/database/migrations/2024_07_23_112710_add_validation_logs_to_servers.php b/database/migrations/2024_07_23_112710_add_validation_logs_to_servers.php new file mode 100644 index 000000000..d3293620b --- /dev/null +++ b/database/migrations/2024_07_23_112710_add_validation_logs_to_servers.php @@ -0,0 +1,28 @@ +text('validation_logs')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->dropColumn('validation_logs'); + }); + } +}; diff --git a/database/migrations/2024_08_05_142659_add_update_frequency_settings.php b/database/migrations/2024_08_05_142659_add_update_frequency_settings.php new file mode 100644 index 000000000..0060b8d1d --- /dev/null +++ b/database/migrations/2024_08_05_142659_add_update_frequency_settings.php @@ -0,0 +1,32 @@ +string('auto_update_frequency')->default('0 0 * * *'); + $table->string('update_check_frequency')->default('0 * * * *'); + $table->boolean('new_version_available')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('instance_settings', function (Blueprint $table) { + $table->dropColumn('update_check_frequency'); + $table->dropColumn('auto_update_frequency'); + $table->dropColumn('new_version_available'); + }); + } +}; diff --git a/database/migrations/2024_08_07_155324_add_proxy_label_chooser.php b/database/migrations/2024_08_07_155324_add_proxy_label_chooser.php new file mode 100644 index 000000000..7bc8a0657 --- /dev/null +++ b/database/migrations/2024_08_07_155324_add_proxy_label_chooser.php @@ -0,0 +1,28 @@ +boolean('generate_exact_labels')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('generate_exact_labels'); + }); + } +}; diff --git a/database/migrations/2024_08_09_215659_add_server_cleanup_fields_to_server_settings_table.php b/database/migrations/2024_08_09_215659_add_server_cleanup_fields_to_server_settings_table.php new file mode 100644 index 000000000..e3bdc68c6 --- /dev/null +++ b/database/migrations/2024_08_09_215659_add_server_cleanup_fields_to_server_settings_table.php @@ -0,0 +1,59 @@ +boolean('force_docker_cleanup')->default(false); + $table->string('docker_cleanup_frequency')->default('*/10 * * * *'); + $table->integer('docker_cleanup_threshold')->default(80); + + // Remove old columns + $table->dropColumn('cleanup_after_percentage'); + $table->dropColumn('is_force_cleanup_enabled'); + }); + foreach ($serverSettings as $serverSetting) { + $serverSetting->force_docker_cleanup = $serverSetting->is_force_cleanup_enabled; + $serverSetting->docker_cleanup_threshold = $serverSetting->cleanup_after_percentage; + $serverSetting->save(); + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + $serverSettings = ServerSetting::all(); + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('force_docker_cleanup'); + $table->dropColumn('docker_cleanup_frequency'); + $table->dropColumn('docker_cleanup_threshold'); + + // Add back old columns + $table->integer('cleanup_after_percentage')->default(80); + $table->boolean('force_server_cleanup')->default(false); + $table->boolean('is_force_cleanup_enabled')->default(false); + }); + foreach ($serverSettings as $serverSetting) { + $serverSetting->is_force_cleanup_enabled = $serverSetting->force_docker_cleanup; + $serverSetting->cleanup_after_percentage = $serverSetting->docker_cleanup_threshold; + $serverSetting->save(); + } + } +} diff --git a/database/migrations/2024_08_12_131659_add_local_file_volume_based_on_git.php b/database/migrations/2024_08_12_131659_add_local_file_volume_based_on_git.php new file mode 100644 index 000000000..d180c7ec2 --- /dev/null +++ b/database/migrations/2024_08_12_131659_add_local_file_volume_based_on_git.php @@ -0,0 +1,28 @@ +boolean('is_based_on_git')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('local_file_volumes', function (Blueprint $table) { + $table->dropColumn('is_based_on_git'); + }); + } +}; diff --git a/database/migrations/2024_08_12_155023_add_timezone_to_server_and_instance_settings.php b/database/migrations/2024_08_12_155023_add_timezone_to_server_and_instance_settings.php new file mode 100644 index 000000000..5bc73a54e --- /dev/null +++ b/database/migrations/2024_08_12_155023_add_timezone_to_server_and_instance_settings.php @@ -0,0 +1,30 @@ +string('server_timezone')->default(''); + }); + + Schema::table('instance_settings', function (Blueprint $table) { + $table->string('instance_timezone')->default('UTC'); + }); + } + + public function down() + { + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('server_timezone'); + }); + + Schema::table('instance_settings', function (Blueprint $table) { + $table->dropColumn('instance_timezone'); + }); + } +} diff --git a/database/migrations/2024_08_14_183120_add_order_to_environment_variables_table.php b/database/migrations/2024_08_14_183120_add_order_to_environment_variables_table.php new file mode 100644 index 000000000..527535827 --- /dev/null +++ b/database/migrations/2024_08_14_183120_add_order_to_environment_variables_table.php @@ -0,0 +1,28 @@ +integer('order')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('environment_variables', function (Blueprint $table) { + $table->dropColumn('order'); + }); + } +}; diff --git a/database/migrations/2024_08_15_115907_add_build_server_id_to_deployment_queue.php b/database/migrations/2024_08_15_115907_add_build_server_id_to_deployment_queue.php new file mode 100644 index 000000000..da5f065a6 --- /dev/null +++ b/database/migrations/2024_08_15_115907_add_build_server_id_to_deployment_queue.php @@ -0,0 +1,28 @@ +integer('build_server_id')->nullable()->after('server_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_deployment_queues', function (Blueprint $table) { + $table->dropColumn('build_server_id'); + }); + } +}; diff --git a/database/migrations/2024_08_16_105649_add_custom_docker_options_to_dbs.php b/database/migrations/2024_08_16_105649_add_custom_docker_options_to_dbs.php new file mode 100644 index 000000000..dbca8c10a --- /dev/null +++ b/database/migrations/2024_08_16_105649_add_custom_docker_options_to_dbs.php @@ -0,0 +1,70 @@ +text('custom_docker_run_options')->nullable(); + }); + Schema::table('standalone_mysqls', function (Blueprint $table) { + $table->text('custom_docker_run_options')->nullable(); + }); + Schema::table('standalone_mariadbs', function (Blueprint $table) { + $table->text('custom_docker_run_options')->nullable(); + }); + Schema::table('standalone_redis', function (Blueprint $table) { + $table->text('custom_docker_run_options')->nullable(); + }); + Schema::table('standalone_clickhouses', function (Blueprint $table) { + $table->text('custom_docker_run_options')->nullable(); + }); + Schema::table('standalone_dragonflies', function (Blueprint $table) { + $table->text('custom_docker_run_options')->nullable(); + }); + Schema::table('standalone_keydbs', function (Blueprint $table) { + $table->text('custom_docker_run_options')->nullable(); + }); + Schema::table('standalone_mongodbs', function (Blueprint $table) { + $table->text('custom_docker_run_options')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('standalone_postgresqls', function (Blueprint $table) { + $table->dropColumn('custom_docker_run_options'); + }); + Schema::table('standalone_mysqls', function (Blueprint $table) { + $table->dropColumn('custom_docker_run_options'); + }); + Schema::table('standalone_mariadbs', function (Blueprint $table) { + $table->dropColumn('custom_docker_run_options'); + }); + Schema::table('standalone_redis', function (Blueprint $table) { + $table->dropColumn('custom_docker_run_options'); + }); + Schema::table('standalone_clickhouses', function (Blueprint $table) { + $table->dropColumn('custom_docker_run_options'); + }); + Schema::table('standalone_dragonflies', function (Blueprint $table) { + $table->dropColumn('custom_docker_run_options'); + }); + Schema::table('standalone_keydbs', function (Blueprint $table) { + $table->dropColumn('custom_docker_run_options'); + }); + Schema::table('standalone_mongodbs', function (Blueprint $table) { + $table->dropColumn('custom_docker_run_options'); + }); + } +}; diff --git a/database/seeders/new_services.php b/database/migrations/2024_08_27_090528_add_compose_parsing_version_to_services.php similarity index 63% rename from database/seeders/new_services.php rename to database/migrations/2024_08_27_090528_add_compose_parsing_version_to_services.php index 77d952734..e8c0474d2 100644 --- a/database/seeders/new_services.php +++ b/database/migrations/2024_08_27_090528_add_compose_parsing_version_to_services.php @@ -12,9 +12,7 @@ return new class extends Migration public function up(): void { Schema::table('services', function (Blueprint $table) { - $table->string('git_repository')->nullable(); - $table->string('git_branch')->nullable(); - $table->nullableMorphs('source'); + $table->string('compose_parsing_version')->default('2'); }); } @@ -24,9 +22,7 @@ return new class extends Migration public function down(): void { Schema::table('services', function (Blueprint $table) { - $table->dropColumn('git_repository'); - $table->dropColumn('git_branch'); - $table->dropMorphs('source'); + $table->dropColumn('compose_parsing_version'); }); } }; diff --git a/database/migrations/2024_09_05_085700_add_helper_version_to_instance_settings.php b/database/migrations/2024_09_05_085700_add_helper_version_to_instance_settings.php new file mode 100644 index 000000000..53ecce0f1 --- /dev/null +++ b/database/migrations/2024_09_05_085700_add_helper_version_to_instance_settings.php @@ -0,0 +1,28 @@ +string('helper_version')->default('1.0.0'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('instance_settings', function (Blueprint $table) { + $table->dropColumn('helper_version'); + }); + } +}; diff --git a/database/migrations/2024_09_06_062534_change_server_cleanup_to_forced.php b/database/migrations/2024_09_06_062534_change_server_cleanup_to_forced.php new file mode 100644 index 000000000..ad6e5bd9e --- /dev/null +++ b/database/migrations/2024_09_06_062534_change_server_cleanup_to_forced.php @@ -0,0 +1,37 @@ +boolean('force_docker_cleanup')->default(true)->change(); + }); + $serverSettings = ServerSetting::all(); + foreach ($serverSettings as $serverSetting) { + if ($serverSetting->force_docker_cleanup === false) { + $serverSetting->force_docker_cleanup = true; + $serverSetting->docker_cleanup_frequency = '*/10 * * * *'; + $serverSetting->save(); + } + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->boolean('force_docker_cleanup')->default(false)->change(); + }); + } +}; diff --git a/database/migrations/2024_09_07_185402_change_cleanup_schedule.php b/database/migrations/2024_09_07_185402_change_cleanup_schedule.php new file mode 100644 index 000000000..ea8861f9b --- /dev/null +++ b/database/migrations/2024_09_07_185402_change_cleanup_schedule.php @@ -0,0 +1,37 @@ +string('docker_cleanup_frequency')->default('0 0 * * *')->change(); + }); + + $serverSettings = ServerSetting::all(); + foreach ($serverSettings as $serverSetting) { + if ($serverSetting->force_docker_cleanup && $serverSetting->docker_cleanup_frequency === '*/10 * * * *') { + $serverSetting->docker_cleanup_frequency = '0 0 * * *'; + $serverSetting->save(); + } + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->string('docker_cleanup_frequency')->default('*/10 * * * *')->change(); + }); + } +}; diff --git a/database/migrations/2024_09_08_130756_update_server_settings_default_timezone.php b/database/migrations/2024_09_08_130756_update_server_settings_default_timezone.php new file mode 100644 index 000000000..109bc40ef --- /dev/null +++ b/database/migrations/2024_09_08_130756_update_server_settings_default_timezone.php @@ -0,0 +1,28 @@ +string('server_timezone')->default('UTC')->change(); + }); + + DB::table('server_settings') + ->whereNull('server_timezone') + ->orWhere('server_timezone', '') + ->update(['server_timezone' => 'UTC']); + } + + public function down() + { + Schema::table('server_settings', function (Blueprint $table) { + $table->string('server_timezone')->default('')->change(); + }); + } +} diff --git a/database/migrations/2024_09_16_111428_encrypt_existing_private_keys.php b/database/migrations/2024_09_16_111428_encrypt_existing_private_keys.php new file mode 100644 index 000000000..e16181ac7 --- /dev/null +++ b/database/migrations/2024_09_16_111428_encrypt_existing_private_keys.php @@ -0,0 +1,24 @@ +chunkById(100, function ($keys) { + foreach ($keys as $key) { + DB::table('private_keys') + ->where('id', $key->id) + ->update(['private_key' => Crypt::encryptString($key->private_key)]); + } + }); + } catch (\Exception $e) { + echo 'Encrypting private keys failed.'; + echo $e->getMessage(); + } + } +} diff --git a/database/migrations/2024_09_17_111226_add_ssh_key_fingerprint_to_private_keys_table.php b/database/migrations/2024_09_17_111226_add_ssh_key_fingerprint_to_private_keys_table.php new file mode 100644 index 000000000..dfce5682a --- /dev/null +++ b/database/migrations/2024_09_17_111226_add_ssh_key_fingerprint_to_private_keys_table.php @@ -0,0 +1,39 @@ +string('fingerprint')->after('private_key')->nullable(); + }); + + try { + DB::table('private_keys')->chunkById(100, function ($keys) { + foreach ($keys as $key) { + $fingerprint = PrivateKey::generateFingerprint($key->private_key); + if ($fingerprint) { + $key->fingerprint = $fingerprint; + $key->save(); + } + } + }); + } catch (\Exception $e) { + echo 'Generating fingerprints failed.'; + echo $e->getMessage(); + } + } + + public function down() + { + Schema::table('private_keys', function (Blueprint $table) { + $table->dropColumn('fingerprint'); + }); + } +} diff --git a/database/migrations/2024_09_22_165240_add_advanced_options_to_cleanup_options_to_servers_settings_table.php b/database/migrations/2024_09_22_165240_add_advanced_options_to_cleanup_options_to_servers_settings_table.php new file mode 100644 index 000000000..b3c58afe3 --- /dev/null +++ b/database/migrations/2024_09_22_165240_add_advanced_options_to_cleanup_options_to_servers_settings_table.php @@ -0,0 +1,24 @@ +boolean('delete_unused_volumes')->default(false); + $table->boolean('delete_unused_networks')->default(false); + }); + } + + public function down() + { + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('delete_unused_volumes'); + $table->dropColumn('delete_unused_networks'); + }); + } +}; diff --git a/database/migrations/2024_09_26_083441_disable_api_by_default.php b/database/migrations/2024_09_26_083441_disable_api_by_default.php new file mode 100644 index 000000000..d0803f751 --- /dev/null +++ b/database/migrations/2024_09_26_083441_disable_api_by_default.php @@ -0,0 +1,18 @@ +boolean('is_api_enabled')->default(false)->change(); + }); + } +}; diff --git a/database/migrations/2024_10_03_095427_add_dump_all_to_standalone_postgresqls.php b/database/migrations/2024_10_03_095427_add_dump_all_to_standalone_postgresqls.php new file mode 100644 index 000000000..b1f301bd3 --- /dev/null +++ b/database/migrations/2024_10_03_095427_add_dump_all_to_standalone_postgresqls.php @@ -0,0 +1,28 @@ +boolean('dump_all')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('scheduled_database_backups', function (Blueprint $table) { + $table->dropColumn('dump_all'); + }); + } +}; diff --git a/database/migrations/2024_10_10_081444_remove_constraint_from_service_applications_fqdn.php b/database/migrations/2024_10_10_081444_remove_constraint_from_service_applications_fqdn.php new file mode 100644 index 000000000..feb144de6 --- /dev/null +++ b/database/migrations/2024_10_10_081444_remove_constraint_from_service_applications_fqdn.php @@ -0,0 +1,34 @@ +dropUnique(['fqdn']); + }); + Schema::table('applications', function (Blueprint $table) { + $table->dropUnique(['fqdn']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('service_applications', function (Blueprint $table) { + $table->unique('fqdn'); + }); + Schema::table('applications', function (Blueprint $table) { + $table->unique('fqdn'); + }); + } +}; diff --git a/database/migrations/2024_10_11_114331_add_required_env_variables.php b/database/migrations/2024_10_11_114331_add_required_env_variables.php new file mode 100644 index 000000000..4fde0c2bb --- /dev/null +++ b/database/migrations/2024_10_11_114331_add_required_env_variables.php @@ -0,0 +1,28 @@ +boolean('is_required')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('environment_variables', function (Blueprint $table) { + $table->dropColumn('is_required'); + }); + } +}; diff --git a/database/migrations/2024_10_14_090416_update_metrics_token_in_server_settings.php b/database/migrations/2024_10_14_090416_update_metrics_token_in_server_settings.php new file mode 100644 index 000000000..d5c38501f --- /dev/null +++ b/database/migrations/2024_10_14_090416_update_metrics_token_in_server_settings.php @@ -0,0 +1,54 @@ +dropColumn('metrics_token'); + $table->dropColumn('metrics_refresh_rate_seconds'); + $table->dropColumn('metrics_history_days'); + $table->dropColumn('is_server_api_enabled'); + + $table->boolean('is_sentinel_enabled')->default(false); + $table->text('sentinel_token')->nullable(); + $table->integer('sentinel_metrics_refresh_rate_seconds')->default(10); + $table->integer('sentinel_metrics_history_days')->default(7); + $table->integer('sentinel_push_interval_seconds')->default(60); + $table->string('sentinel_custom_url')->nullable(); + }); + Schema::table('servers', function (Blueprint $table) { + $table->dateTime('sentinel_updated_at')->default(now()); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->string('metrics_token')->nullable(); + $table->integer('metrics_refresh_rate_seconds')->default(5); + $table->integer('metrics_history_days')->default(30); + $table->boolean('is_server_api_enabled')->default(false); + + $table->dropColumn('is_sentinel_enabled'); + $table->dropColumn('sentinel_token'); + $table->dropColumn('sentinel_metrics_refresh_rate_seconds'); + $table->dropColumn('sentinel_metrics_history_days'); + $table->dropColumn('sentinel_push_interval_seconds'); + $table->dropColumn('sentinel_custom_url'); + }); + Schema::table('servers', function (Blueprint $table) { + $table->dropColumn('sentinel_updated_at'); + }); + } +}; diff --git a/database/migrations/2024_10_15_172139_add_is_shared_to_environment_variables.php b/database/migrations/2024_10_15_172139_add_is_shared_to_environment_variables.php new file mode 100644 index 000000000..eb878e2f6 --- /dev/null +++ b/database/migrations/2024_10_15_172139_add_is_shared_to_environment_variables.php @@ -0,0 +1,22 @@ +boolean('is_shared')->default(false); + }); + } + + public function down() + { + Schema::table('environment_variables', function (Blueprint $table) { + $table->dropColumn('is_shared'); + }); + } +} diff --git a/database/migrations/2024_10_16_120026_move_redis_password_to_envs.php b/database/migrations/2024_10_16_120026_move_redis_password_to_envs.php new file mode 100644 index 000000000..fa01e8e85 --- /dev/null +++ b/database/migrations/2024_10_16_120026_move_redis_password_to_envs.php @@ -0,0 +1,41 @@ +where('id', $redis->id)->value('redis_password'); + EnvironmentVariable::create([ + 'standalone_redis_id' => $redis->id, + 'key' => 'REDIS_PASSWORD', + 'value' => $redis_password, + ]); + EnvironmentVariable::create([ + 'standalone_redis_id' => $redis->id, + 'key' => 'REDIS_USERNAME', + 'value' => 'default', + ]); + } + }); + Schema::table('standalone_redis', function (Blueprint $table) { + $table->dropColumn('redis_password'); + }); + } catch (\Exception $e) { + echo 'Moving Redis passwords to envs failed.'; + echo $e->getMessage(); + } + } +} diff --git a/database/migrations/2024_10_16_192133_add_confirmation_settings_to_instance_settings_table.php b/database/migrations/2024_10_16_192133_add_confirmation_settings_to_instance_settings_table.php new file mode 100644 index 000000000..7040daf44 --- /dev/null +++ b/database/migrations/2024_10_16_192133_add_confirmation_settings_to_instance_settings_table.php @@ -0,0 +1,22 @@ +boolean('disable_two_step_confirmation')->default(false); + }); + } + + public function down() + { + Schema::table('instance_settings', function (Blueprint $table) { + $table->dropColumn('disable_two_step_confirmation'); + }); + } +}; diff --git a/database/migrations/2024_10_17_093722_add_soft_delete_to_servers.php b/database/migrations/2024_10_17_093722_add_soft_delete_to_servers.php new file mode 100644 index 000000000..7a7f28e24 --- /dev/null +++ b/database/migrations/2024_10_17_093722_add_soft_delete_to_servers.php @@ -0,0 +1,28 @@ +softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->dropSoftDeletes(); + }); + } +}; diff --git a/database/migrations/2024_10_22_105745_add_server_disk_usage_threshold.php b/database/migrations/2024_10_22_105745_add_server_disk_usage_threshold.php new file mode 100644 index 000000000..76ccd1352 --- /dev/null +++ b/database/migrations/2024_10_22_105745_add_server_disk_usage_threshold.php @@ -0,0 +1,28 @@ +integer('server_disk_usage_notification_threshold')->default(80)->after('docker_cleanup_threshold'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('server_disk_usage_notification_threshold'); + }); + } +}; diff --git a/database/migrations/2024_10_22_121223_add_server_disk_usage_notification.php b/database/migrations/2024_10_22_121223_add_server_disk_usage_notification.php new file mode 100644 index 000000000..a2aa381b7 --- /dev/null +++ b/database/migrations/2024_10_22_121223_add_server_disk_usage_notification.php @@ -0,0 +1,32 @@ +boolean('discord_notifications_server_disk_usage')->default(true)->after('discord_enabled'); + $table->boolean('smtp_notifications_server_disk_usage')->default(true)->after('smtp_enabled'); + $table->boolean('telegram_notifications_server_disk_usage')->default(true)->after('telegram_enabled'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('teams', function (Blueprint $table) { + $table->dropColumn('discord_notifications_server_disk_usage'); + $table->dropColumn('smtp_notifications_server_disk_usage'); + $table->dropColumn('telegram_notifications_server_disk_usage'); + }); + } +}; diff --git a/database/migrations/2024_10_29_093927_add_is_sentinel_debug_enabled_to_server_settings.php b/database/migrations/2024_10_29_093927_add_is_sentinel_debug_enabled_to_server_settings.php new file mode 100644 index 000000000..d8ab1313b --- /dev/null +++ b/database/migrations/2024_10_29_093927_add_is_sentinel_debug_enabled_to_server_settings.php @@ -0,0 +1,28 @@ +boolean('is_sentinel_debug_enabled')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('is_sentinel_debug_enabled'); + }); + } +}; diff --git a/database/migrations/2024_11_02_213214_add_last_online_at_to_resources.php b/database/migrations/2024_11_02_213214_add_last_online_at_to_resources.php new file mode 100644 index 000000000..51b8fb3ba --- /dev/null +++ b/database/migrations/2024_11_02_213214_add_last_online_at_to_resources.php @@ -0,0 +1,96 @@ +timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('application_previews', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('service_applications', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('service_databases', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_postgresqls', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_redis', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_mongodbs', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_mysqls', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_mariadbs', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_keydbs', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_dragonflies', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_clickhouses', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('application_previews', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('service_applications', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('service_databases', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_postgresqls', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_redis', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_mongodbs', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_mysqls', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_mariadbs', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_keydbs', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_dragonflies', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_clickhouses', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + + } +}; diff --git a/database/migrations/2024_11_11_125335_add_custom_nginx_configuration_to_static.php b/database/migrations/2024_11_11_125335_add_custom_nginx_configuration_to_static.php new file mode 100644 index 000000000..1c6e2daa0 --- /dev/null +++ b/database/migrations/2024_11_11_125335_add_custom_nginx_configuration_to_static.php @@ -0,0 +1,28 @@ +longText('custom_nginx_configuration')->nullable()->after('static_image'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('custom_nginx_configuration'); + }); + } +}; diff --git a/database/seeders/ApplicationPreviewSeeder.php b/database/seeders/ApplicationPreviewSeeder.php deleted file mode 100644 index 764939073..000000000 --- a/database/seeders/ApplicationPreviewSeeder.php +++ /dev/null @@ -1,20 +0,0 @@ - $application_1->id, - // 'pull_request_id' => 1 - // ]); - } -} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index b3fac350f..6e66c64f4 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -13,26 +13,21 @@ class DatabaseSeeder extends Seeder UserSeeder::class, TeamSeeder::class, PrivateKeySeeder::class, + PopulateSshKeysDirectorySeeder::class, ServerSeeder::class, ServerSettingSeeder::class, ProjectSeeder::class, - ProjectSettingSeeder::class, - EnvironmentSeeder::class, StandaloneDockerSeeder::class, - SwarmDockerSeeder::class, - KubernetesSeeder::class, GithubAppSeeder::class, GitlabAppSeeder::class, ApplicationSeeder::class, ApplicationSettingsSeeder::class, - ApplicationPreviewSeeder::class, - EnvironmentVariableSeeder::class, LocalPersistentVolumeSeeder::class, S3StorageSeeder::class, StandalonePostgresqlSeeder::class, - ScheduledDatabaseBackupSeeder::class, - ScheduledDatabaseBackupExecutionSeeder::class, OauthSettingSeeder::class, + DisableTwoStepConfirmationSeeder::class, + SentinelSeeder::class, ]); } } diff --git a/database/seeders/DisableTwoStepConfirmationSeeder.php b/database/seeders/DisableTwoStepConfirmationSeeder.php new file mode 100644 index 000000000..c43bf1b01 --- /dev/null +++ b/database/seeders/DisableTwoStepConfirmationSeeder.php @@ -0,0 +1,20 @@ +updateOrInsert( + [], + ['disable_two_step_confirmation' => true] + ); + } +} diff --git a/database/seeders/EnvironmentSeeder.php b/database/seeders/EnvironmentSeeder.php deleted file mode 100644 index 0e980f22b..000000000 --- a/database/seeders/EnvironmentSeeder.php +++ /dev/null @@ -1,15 +0,0 @@ - 'NODE_ENV', - // 'value' => 'production', - // 'is_build_time' => true, - // 'application_id' => 1, - // ]); - } -} diff --git a/database/seeders/GitSeeder.php b/database/seeders/GitSeeder.php deleted file mode 100644 index c8dc3ab6d..000000000 --- a/database/seeders/GitSeeder.php +++ /dev/null @@ -1,26 +0,0 @@ - 'https://api.github.com', - // 'html_url' => 'https://github.com', - // 'is_public' => false, - // 'private_key_id' => $private_key_1->id, - // 'project_id' => $project->id, - // ]); - } -} diff --git a/database/seeders/GithubAppSeeder.php b/database/seeders/GithubAppSeeder.php index 4aa5ec753..2ece7a05b 100644 --- a/database/seeders/GithubAppSeeder.php +++ b/database/seeders/GithubAppSeeder.php @@ -31,7 +31,7 @@ class GithubAppSeeder extends Seeder 'client_id' => 'Iv1.220e564d2b0abd8c', 'client_secret' => '116d1d80289f378410dd70ab4e4b81dd8d2c52b6', 'webhook_secret' => '326a47b49054f03288f800d81247ec9414d0abf3', - 'private_key_id' => 1, + 'private_key_id' => 2, 'team_id' => 0, ]); } diff --git a/database/seeders/GitlabAppSeeder.php b/database/seeders/GitlabAppSeeder.php index af63f2ed7..ec2b7ec5e 100644 --- a/database/seeders/GitlabAppSeeder.php +++ b/database/seeders/GitlabAppSeeder.php @@ -20,19 +20,5 @@ class GitlabAppSeeder extends Seeder 'is_public' => true, 'team_id' => 0, ]); - GitlabApp::create([ - 'id' => 2, - 'name' => 'coolify-laravel-development-private-gitlab', - 'api_url' => 'https://gitlab.com/api/v4', - 'html_url' => 'https://gitlab.com', - 'app_id' => 1234, - 'app_secret' => '1234', - 'oauth_id' => 1234, - 'deploy_key_id' => '1234', - 'public_key' => 'dfjasiourj', - 'webhook_token' => '4u3928u4y392', - 'private_key_id' => 2, - 'team_id' => 0, - ]); } } diff --git a/database/seeders/InstanceSettingsSeeder.php b/database/seeders/InstanceSettingsSeeder.php index 31c8cfb5f..35fc8506b 100644 --- a/database/seeders/InstanceSettingsSeeder.php +++ b/database/seeders/InstanceSettingsSeeder.php @@ -27,14 +27,14 @@ class InstanceSettingsSeeder extends Seeder $ipv4 = Process::run('curl -4s https://ifconfig.io')->output(); $ipv4 = trim($ipv4); $ipv4 = filter_var($ipv4, FILTER_VALIDATE_IP); - $settings = InstanceSettings::get(); + $settings = instanceSettings(); if (is_null($settings->public_ipv4) && $ipv4) { $settings->update(['public_ipv4' => $ipv4]); } $ipv6 = Process::run('curl -6s https://ifconfig.io')->output(); $ipv6 = trim($ipv6); $ipv6 = filter_var($ipv6, FILTER_VALIDATE_IP); - $settings = InstanceSettings::get(); + $settings = instanceSettings(); if (is_null($settings->public_ipv6) && $ipv6) { $settings->update(['public_ipv6' => $ipv6]); } diff --git a/database/seeders/KubernetesSeeder.php b/database/seeders/KubernetesSeeder.php deleted file mode 100644 index f6a852e05..000000000 --- a/database/seeders/KubernetesSeeder.php +++ /dev/null @@ -1,16 +0,0 @@ -deleteDirectory(''); + Storage::disk('ssh-keys')->makeDirectory(''); + Storage::disk('ssh-mux')->deleteDirectory(''); + Storage::disk('ssh-mux')->makeDirectory(''); + + PrivateKey::chunk(100, function ($keys) { + foreach ($keys as $key) { + $key->storeInFileSystem(); + } + }); + + if (isDev()) { + $user = env('PUID').':'.env('PGID'); + Process::run("chown -R $user ".storage_path('app/ssh/keys')); + Process::run("chown -R $user ".storage_path('app/ssh/mux')); + } else { + Process::run('chown -R 9999:root '.storage_path('app/ssh/keys')); + Process::run('chown -R 9999:root '.storage_path('app/ssh/mux')); + } + } catch (\Throwable $e) { + echo "Error: {$e->getMessage()}\n"; + } + } +} diff --git a/database/seeders/PrivateKeySeeder.php b/database/seeders/PrivateKeySeeder.php index 8a70cf56d..6b44d0867 100644 --- a/database/seeders/PrivateKeySeeder.php +++ b/database/seeders/PrivateKeySeeder.php @@ -13,9 +13,8 @@ class PrivateKeySeeder extends Seeder public function run(): void { PrivateKey::create([ - 'id' => 0, 'team_id' => 0, - 'name' => 'Testing-host', + 'name' => 'Testing Host Key', 'description' => 'This is a test docker container', 'private_key' => '-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW @@ -25,10 +24,9 @@ AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== -----END OPENSSH PRIVATE KEY----- ', - ]); + PrivateKey::create([ - 'id' => 1, 'team_id' => 0, 'name' => 'development-github-app', 'description' => 'This is the key for using the development GitHub app', @@ -61,12 +59,5 @@ a1C8EDKapCw5hAhizEFOUQKOygL8Ipn+tmEUkORYdZ8Q8cWFCv9nIw== -----END RSA PRIVATE KEY-----', 'is_git_related' => true, ]); - PrivateKey::create([ - 'id' => 2, - 'team_id' => 0, - 'name' => 'development-gitlab-app', - 'description' => 'This is the key for using the development Gitlab app', - 'private_key' => 'asdf', - ]); } } diff --git a/database/seeders/ProductionSeeder.php b/database/seeders/ProductionSeeder.php index 5db2e826c..3e820a162 100644 --- a/database/seeders/ProductionSeeder.php +++ b/database/seeders/ProductionSeeder.php @@ -64,31 +64,8 @@ class ProductionSeeder extends Seeder 'team_id' => 0, ]); } - - if (! isCloud() && config('coolify.is_windows_docker_desktop') == false) { - echo "Checking localhost key.\n"; - // Save SSH Keys for the Coolify Host - $coolify_key_name = 'id.root@host.docker.internal'; - $coolify_key = Storage::disk('ssh-keys')->get("{$coolify_key_name}"); - - if ($coolify_key) { - PrivateKey::updateOrCreate( - [ - 'id' => 0, - 'team_id' => 0, - ], - [ - 'name' => 'localhost\'s key', - 'description' => 'The private key for the Coolify host machine (localhost).', 'private_key' => $coolify_key, - ] - ); - } else { - echo "No SSH key found for the Coolify host machine (localhost).\n"; - echo "Please generate one and save it in /data/coolify/ssh/keys/{$coolify_key_name}\n"; - echo "Then try to install again.\n"; - exit(1); - } - // Add Coolify host (localhost) as Server if it doesn't exist + // Add Coolify host (localhost) as Server if it doesn't exist + if (! isCloud()) { if (Server::find(0) == null) { $server_details = [ 'id' => 0, @@ -100,7 +77,7 @@ class ProductionSeeder extends Seeder 'private_key_id' => 0, ]; $server_details['proxy'] = ServerMetadata::from([ - 'type' => ProxyTypes::TRAEFIK_V2->value, + 'type' => ProxyTypes::TRAEFIK->value, 'status' => ProxyStatus::EXITED->value, ]); $server = Server::create($server_details); @@ -122,6 +99,34 @@ class ProductionSeeder extends Seeder ]); } } + + if (! isCloud() && config('coolify.is_windows_docker_desktop') == false) { + $coolify_key_name = '@host.docker.internal'; + $ssh_keys_directory = Storage::disk('ssh-keys')->files(); + $coolify_key = collect($ssh_keys_directory)->firstWhere(fn ($item) => str($item)->contains($coolify_key_name)); + + $server = Server::find(0); + $found = $server->privateKey; + if (! $found) { + if ($coolify_key) { + $user = str($coolify_key)->before('@')->after('id.'); + $coolify_key = Storage::disk('ssh-keys')->get($coolify_key); + PrivateKey::create([ + 'id' => 0, + 'team_id' => 0, + 'name' => 'localhost\'s key', + 'description' => 'The private key for the Coolify host machine (localhost).', + 'private_key' => $coolify_key, + ]); + $server->update(['user' => $user]); + echo "SSH key found for the Coolify host machine (localhost).\n"; + } else { + echo "No SSH key found for the Coolify host machine (localhost).\n"; + echo "Please read the following documentation (point 3) to fix it: https://coolify.io/docs/knowledge-base/server/openssh/\n"; + echo "Your localhost connection won't work until then."; + } + } + } if (config('coolify.is_windows_docker_desktop')) { PrivateKey::updateOrCreate( [ @@ -153,7 +158,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== 'private_key_id' => 0, ]; $server_details['proxy'] = ServerMetadata::from([ - 'type' => ProxyTypes::TRAEFIK_V2->value, + 'type' => ProxyTypes::TRAEFIK->value, 'status' => ProxyStatus::EXITED->value, ]); $server = Server::create($server_details); @@ -178,7 +183,8 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== get_public_ips(); - $oauth_settings_seeder = new OauthSettingSeeder(); - $oauth_settings_seeder->run(); + $this->call(OauthSettingSeeder::class); + $this->call(PopulateSshKeysDirectorySeeder::class); + $this->call(SentinelSeeder::class); } } diff --git a/database/seeders/ProjectSettingSeeder.php b/database/seeders/ProjectSettingSeeder.php deleted file mode 100644 index 2a8cdfdb4..000000000 --- a/database/seeders/ProjectSettingSeeder.php +++ /dev/null @@ -1,15 +0,0 @@ -settings->wildcard_domain = 'wildcard.example.com'; - // $first_project->settings->save(); - } -} diff --git a/database/seeders/ScheduledDatabaseBackupExecutionSeeder.php b/database/seeders/ScheduledDatabaseBackupExecutionSeeder.php deleted file mode 100644 index 7e4c33764..000000000 --- a/database/seeders/ScheduledDatabaseBackupExecutionSeeder.php +++ /dev/null @@ -1,28 +0,0 @@ - 'success', - // 'message' => 'Backup created successfully.', - // 'size' => '10243467789556', - // 'scheduled_database_backup_id' => 1, - // ]); - // ScheduledDatabaseBackupExecution::create([ - // 'status' => 'failed', - // 'message' => 'Backup failed.', - // 'size' => '10243456', - // 'scheduled_database_backup_id' => 1, - // ]); - } -} diff --git a/database/seeders/ScheduledDatabaseBackupSeeder.php b/database/seeders/ScheduledDatabaseBackupSeeder.php deleted file mode 100644 index fefbada0d..000000000 --- a/database/seeders/ScheduledDatabaseBackupSeeder.php +++ /dev/null @@ -1,33 +0,0 @@ - true, - // 'frequency' => '* * * * *', - // 'number_of_backups_locally' => 2, - // 'database_id' => 1, - // 'database_type' => 'App\Models\StandalonePostgresql', - // 's3_storage_id' => 1, - // 'team_id' => 0, - // ]); - // ScheduledDatabaseBackup::create([ - // 'enabled' => true, - // 'frequency' => '* * * * *', - // 'number_of_backups_locally' => 3, - // 'database_id' => 1, - // 'database_type' => 'App\Models\StandalonePostgresql', - // 'team_id' => 0, - // ]); - } -} diff --git a/database/seeders/SentinelSeeder.php b/database/seeders/SentinelSeeder.php new file mode 100644 index 000000000..3cf913933 --- /dev/null +++ b/database/seeders/SentinelSeeder.php @@ -0,0 +1,32 @@ +settings->sentinel_token)->isEmpty()) { + $server->settings->generateSentinelToken(ignoreEvent: true); + } + if (str($server->settings->sentinel_custom_url)->isEmpty()) { + $url = $server->settings->generateSentinelUrl(ignoreEvent: true); + if (str($url)->isEmpty()) { + $server->settings->is_sentinel_enabled = false; + $server->settings->save(); + } + } + } catch (\Throwable $e) { + Log::error('Error seeding sentinel: '.$e->getMessage()); + } + } + }); + } +} diff --git a/database/seeders/ServerSeeder.php b/database/seeders/ServerSeeder.php index 12594bcb9..d32843107 100644 --- a/database/seeders/ServerSeeder.php +++ b/database/seeders/ServerSeeder.php @@ -2,6 +2,8 @@ namespace Database\Seeders; +use App\Enums\ProxyStatus; +use App\Enums\ProxyTypes; use App\Models\Server; use Illuminate\Database\Seeder; @@ -15,7 +17,11 @@ class ServerSeeder extends Seeder 'description' => 'This is a test docker container in development mode', 'ip' => 'coolify-testing-host', 'team_id' => 0, - 'private_key_id' => 0, + 'private_key_id' => 1, + 'proxy' => [ + 'type' => ProxyTypes::TRAEFIK->value, + 'status' => ProxyStatus::EXITED->value, + ], ]); } } diff --git a/database/seeders/ServiceApplicationSeeder.php b/database/seeders/ServiceApplicationSeeder.php deleted file mode 100644 index 04648f83c..000000000 --- a/database/seeders/ServiceApplicationSeeder.php +++ /dev/null @@ -1,16 +0,0 @@ - 0, - 'name' => 'Standalone Docker 1', - 'network' => 'coolify', - 'server_id' => 0, - ]); + if (StandaloneDocker::find(0) == null) { + StandaloneDocker::create([ + 'id' => 0, + 'name' => 'Standalone Docker 1', + 'network' => 'coolify', + 'server_id' => 0, + ]); + } } } diff --git a/database/seeders/SubscriptionSeeder.php b/database/seeders/SubscriptionSeeder.php deleted file mode 100644 index 03a5ed8f3..000000000 --- a/database/seeders/SubscriptionSeeder.php +++ /dev/null @@ -1,16 +0,0 @@ - 'Swarm Docker 1', - // 'server_id' => 1, - // ]); - } -} diff --git a/database/seeders/WaitlistSeeder.php b/database/seeders/WaitlistSeeder.php deleted file mode 100644 index de6837c60..000000000 --- a/database/seeders/WaitlistSeeder.php +++ /dev/null @@ -1,16 +0,0 @@ -= 0.8" + } + }, + "node_modules/cookie": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.1.tgz", + "integrity": "sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nan": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "license": "MIT" + }, + "node_modules/node-pty": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", + "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "nan": "^2.17.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/docker/coolify-realtime/package.json b/docker/coolify-realtime/package.json new file mode 100644 index 000000000..faeb80f54 --- /dev/null +++ b/docker/coolify-realtime/package.json @@ -0,0 +1,13 @@ +{ + "private": true, + "type": "module", + "dependencies": { + "@xterm/addon-fit": "0.10.0", + "@xterm/xterm": "5.5.0", + "cookie": "1.0.1", + "axios": "1.7.5", + "dotenv": "16.4.5", + "node-pty": "1.0.0", + "ws": "8.18.0" + } +} diff --git a/docker/coolify-realtime/soketi-entrypoint.sh b/docker/coolify-realtime/soketi-entrypoint.sh new file mode 100644 index 000000000..3bb85bdeb --- /dev/null +++ b/docker/coolify-realtime/soketi-entrypoint.sh @@ -0,0 +1,35 @@ +#!/bin/sh +# Function to timestamp logs + +# Check if the first argument is 'watch' +if [ "$1" = "watch" ]; then + WATCH_MODE="--watch" +else + WATCH_MODE="" +fi + +timestamp() { + date "+%Y-%m-%d %H:%M:%S" +} + +# Start the terminal server in the background with logging +node $WATCH_MODE /terminal/terminal-server.js > >(while read line; do echo "$(timestamp) [TERMINAL] $line"; done) 2>&1 & +TERMINAL_PID=$! + +# Start the Soketi process in the background with logging +node /app/bin/server.js start > >(while read line; do echo "$(timestamp) [SOKETI] $line"; done) 2>&1 & +SOKETI_PID=$! + +# Function to forward signals to child processes +forward_signal() { + kill -$1 $TERMINAL_PID $SOKETI_PID +} + +# Forward SIGTERM to child processes +trap 'forward_signal TERM' TERM + +# Wait for any process to exit +wait -n + +# Exit with status of process that exited first +exit $? diff --git a/docker/coolify-realtime/terminal-server.js b/docker/coolify-realtime/terminal-server.js new file mode 100755 index 000000000..6633204b2 --- /dev/null +++ b/docker/coolify-realtime/terminal-server.js @@ -0,0 +1,239 @@ +import { WebSocketServer } from 'ws'; +import http from 'http'; +import pty from 'node-pty'; +import axios from 'axios'; +import cookie from 'cookie'; +import 'dotenv/config' + +const server = http.createServer((req, res) => { + if (req.url === '/ready') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('OK'); + } else { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not Found'); + } +}); + +const verifyClient = async (info, callback) => { + const cookies = cookie.parse(info.req.headers.cookie || ''); + // const origin = new URL(info.origin); + // const protocol = origin.protocol; + const xsrfToken = cookies['XSRF-TOKEN']; + + // Generate session cookie name based on APP_NAME + const appName = process.env.APP_NAME || 'laravel'; + const sessionCookieName = `${appName.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}_session`; + const laravelSession = cookies[sessionCookieName]; + + // Verify presence of required tokens + if (!laravelSession || !xsrfToken) { + return callback(false, 401, 'Unauthorized: Missing required tokens'); + } + + try { + // Authenticate with Laravel backend + const response = await axios.post(`http://coolify/terminal/auth`, null, { + headers: { + 'Cookie': `${sessionCookieName}=${laravelSession}`, + 'X-XSRF-TOKEN': xsrfToken + }, + }); + + if (response.status === 200) { + // Authentication successful + callback(true); + } else { + callback(false, 401, 'Unauthorized: Invalid credentials'); + } + } catch (error) { + console.error('Authentication error:', error.message); + callback(false, 500, 'Internal Server Error'); + } +}; + + +const wss = new WebSocketServer({ server, path: '/terminal/ws', verifyClient: verifyClient }); +const userSessions = new Map(); + +wss.on('connection', (ws) => { + const userId = generateUserId(); + const userSession = { ws, userId, ptyProcess: null, isActive: false }; + userSessions.set(userId, userSession); + + ws.on('message', (message) => { + handleMessage(userSession, message); + + }); + ws.on('error', (err) => handleError(err, userId)); + ws.on('close', () => handleClose(userId)); + +}); + +const messageHandlers = { + message: (session, data) => session.ptyProcess.write(data), + resize: (session, { cols, rows }) => { + cols = cols > 0 ? cols : 80; + rows = rows > 0 ? rows : 30; + session.ptyProcess.resize(cols, rows) + }, + pause: (session) => session.ptyProcess.pause(), + resume: (session) => session.ptyProcess.resume(), + checkActive: (session, data) => { + if (data === 'force' && session.isActive) { + killPtyProcess(session.userId); + } else { + session.ws.send(session.isActive); + } + }, + command: (session, data) => handleCommand(session.ws, data, session.userId) +}; + +function handleMessage(userSession, message) { + const parsed = parseMessage(message); + if (!parsed) return; + + Object.entries(parsed).forEach(([key, value]) => { + const handler = messageHandlers[key]; + if (handler && (userSession.isActive || key === 'checkActive' || key === 'command')) { + handler(userSession, value); + } + }); +} + +function parseMessage(message) { + try { + return JSON.parse(message); + } catch (e) { + console.error('Failed to parse message:', e); + return null; + } +} + +async function handleCommand(ws, command, userId) { + const userSession = userSessions.get(userId); + if (userSession && userSession.isActive) { + const result = await killPtyProcess(userId); + if (!result) { + // if terminal is still active, even after we tried to kill it, dont continue and show error + ws.send('unprocessable'); + return; + } + } + + const commandString = command[0].split('\n').join(' '); + const timeout = extractTimeout(commandString); + const sshArgs = extractSshArgs(commandString); + const hereDocContent = extractHereDocContent(commandString); + const options = { + name: 'xterm-color', + cols: 80, + rows: 30, + cwd: process.env.HOME, + env: {}, + }; + + // NOTE: - Initiates a process within the Terminal container + // Establishes an SSH connection to root@coolify with RequestTTY enabled + // Executes the 'docker exec' command to connect to a specific container + const ptyProcess = pty.spawn('ssh', sshArgs.concat([hereDocContent]), options); + + userSession.ptyProcess = ptyProcess; + userSession.isActive = true; + + ws.send('pty-ready'); + + ptyProcess.onData((data) => { + ws.send(data); + }); + + // when parent closes + ptyProcess.onExit(({ exitCode, signal }) => { + console.error(`Process exited with code ${exitCode} and signal ${signal}`); + ws.send('pty-exited'); + userSession.isActive = false; + + }); + + if (timeout) { + setTimeout(async () => { + await killPtyProcess(userId); + }, timeout * 1000); + } +} + +async function handleError(err, userId) { + console.error('WebSocket error:', err); + await killPtyProcess(userId); +} + +async function handleClose(userId) { + await killPtyProcess(userId); + userSessions.delete(userId); +} + +async function killPtyProcess(userId) { + const session = userSessions.get(userId); + if (!session?.ptyProcess) return false; + + return new Promise((resolve) => { + // Loop to ensure terminal is killed before continuing + let killAttempts = 0; + const maxAttempts = 5; + + const attemptKill = () => { + killAttempts++; + + // session.ptyProcess.kill() wont work here because of https://github.com/moby/moby/issues/9098 + // patch with https://github.com/moby/moby/issues/9098#issuecomment-189743947 + session.ptyProcess.write('set +o history\nkill -TERM -$$ && exit\nset -o history\n'); + + setTimeout(() => { + if (!session.isActive || !session.ptyProcess) { + resolve(true); + return; + } + + if (killAttempts < maxAttempts) { + attemptKill(); + } else { + resolve(false); + } + }, 500); + }; + + attemptKill(); + }); +} + +function generateUserId() { + return Math.random().toString(36).substring(2, 11); +} + +function extractTimeout(commandString) { + const timeoutMatch = commandString.match(/timeout (\d+)/); + return timeoutMatch ? parseInt(timeoutMatch[1], 10) : null; +} + +function extractSshArgs(commandString) { + const sshCommandMatch = commandString.match(/ssh (.+?) 'bash -se'/); + let sshArgs = sshCommandMatch ? sshCommandMatch[1].split(' ') : []; + sshArgs = sshArgs.map(arg => arg === 'RequestTTY=no' ? 'RequestTTY=yes' : arg); + if (!sshArgs.includes('RequestTTY=yes')) { + sshArgs.push('-o', 'RequestTTY=yes'); + } + return sshArgs; +} + +function extractHereDocContent(commandString) { + const delimiterMatch = commandString.match(/<< (\S+)/); + const delimiter = delimiterMatch ? delimiterMatch[1] : null; + const escapedDelimiter = delimiter.slice(1).trim().replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'); + const hereDocRegex = new RegExp(`<< \\\\${escapedDelimiter}([\\s\\S\\.]*?)${escapedDelimiter}`); + const hereDocMatch = commandString.match(hereDocRegex); + return hereDocMatch ? hereDocMatch[1] : ''; +} + +server.listen(6002, () => { + console.log('Coolify realtime terminal server listening on port 6002. Let the hacking begin!'); +}); diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index f75a0ff1e..d2381f764 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -5,38 +5,45 @@ ARG TARGETPLATFORM ARG CLOUDFLARED_VERSION=2024.4.1 ARG POSTGRES_VERSION=15 -RUN apt-get update -# Postgres version requirements -RUN apt install dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl -y -RUN curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null -RUN echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ jammy-pgdg main | tee -a /etc/apt/sources.list.d/postgresql.list +# Use build arguments for caching +ARG BUILDTIME_DEPS="dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl" +ARG RUNTIME_DEPS="postgresql-client-$POSTGRES_VERSION php8.2-pgsql openssh-client git git-lfs jq lsof" -RUN apt-get update -RUN apt-get install postgresql-client-$POSTGRES_VERSION -y +# Install dependencies +RUN --mount=type=cache,target=/var/cache/apt \ + apt-get update && \ + apt-get install -y $BUILDTIME_DEPS && \ + curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null && \ + echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ jammy-pgdg main | tee -a /etc/apt/sources.list.d/postgresql.list && \ + apt-get update && \ + apt-get install -y $RUNTIME_DEPS && \ + apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* -# Coolify requirements -RUN apt-get install -y php8.2-pgsql openssh-client git git-lfs jq lsof -RUN apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* COPY --chmod=755 docker/dev/etc/s6-overlay/ /etc/s6-overlay/ COPY docker/dev/nginx.conf /etc/nginx/conf.d/custom.conf -RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc -RUN echo "alias a='php artisan'" >>/etc/bash.bashrc +RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc && \ + echo "alias a='php artisan'" >>/etc/bash.bashrc RUN mkdir -p /usr/local/bin -RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \ +RUN --mount=type=cache,target=/root/.cache \ + /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \ echo 'amd64' && \ curl -sSL https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \ ;fi" -RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \ +RUN --mount=type=cache,target=/root/.cache \ + /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \ echo 'arm64' && \ curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \ ;fi" +COPY --from=minio/mc:RELEASE.2024-09-09T07-53-10Z /usr/bin/mc /usr/bin/mc +RUN chmod +x /usr/bin/mc + RUN { \ echo 'upload_max_filesize=256M'; \ echo 'post_max_size=256M'; \ diff --git a/docker/dev/etc/s6-overlay/s6-rc.d/init-setup/up b/docker/dev/etc/s6-overlay/s6-rc.d/init-setup/up index e974e54cc..e02307e49 100644 --- a/docker/dev/etc/s6-overlay/s6-rc.d/init-setup/up +++ b/docker/dev/etc/s6-overlay/s6-rc.d/init-setup/up @@ -1,5 +1,5 @@ #!/command/execlineb -P foreground { composer -d /var/www/html/ install } foreground { php /var/www/html/artisan migrate --step } -foreground { php /var/www/html/artisan dev:init } +foreground { php /var/www/html/artisan dev --init } diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index 4ee3fade2..37e0481bb 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -1,10 +1,10 @@ -FROM serversideup/php:8.2-fpm-nginx-v2.2.1 as base +FROM serversideup/php:8.2-fpm-nginx-v2.2.1 AS base WORKDIR /var/www/html COPY composer.json composer.lock ./ RUN composer install --no-dev --no-interaction --no-plugins --no-scripts --prefer-dist -FROM node:20 as static-assets +FROM node:20 AS static-assets WORKDIR /app COPY . . COPY --from=base --chown=9999:9999 /var/www/html . @@ -17,6 +17,7 @@ ARG TARGETPLATFORM # https://github.com/cloudflare/cloudflared/releases ARG CLOUDFLARED_VERSION=2024.4.1 ARG POSTGRES_VERSION=15 +ARG CI=true WORKDIR /var/www/html @@ -44,6 +45,8 @@ RUN composer dump-autoload COPY --from=static-assets --chown=9999:9999 /app/public/build ./public/build COPY --chmod=755 docker/prod/etc/s6-overlay/ /etc/s6-overlay/ +RUN php artisan route:clear +RUN php artisan view:clear RUN php artisan route:cache RUN php artisan view:cache @@ -67,3 +70,6 @@ RUN { \ echo 'upload_max_filesize=256M'; \ echo 'post_max_size=256M'; \ } > /etc/php/current_version/cli/conf.d/upload-limits.ini + +COPY --from=minio/mc:RELEASE.2024-09-09T07-53-10Z /usr/bin/mc /usr/bin/mc +RUN chmod +x /usr/bin/mc diff --git a/docker/prod/etc/s6-overlay/s6-rc.d/init-script/up b/docker/prod/etc/s6-overlay/s6-rc.d/init-script/up index ea960df95..3b252b782 100644 --- a/docker/prod/etc/s6-overlay/s6-rc.d/init-script/up +++ b/docker/prod/etc/s6-overlay/s6-rc.d/init-script/up @@ -1,3 +1,3 @@ #!/command/execlineb -P s6-setuidgid webuser -php /var/www/html/artisan app:init --full-cleanup +php /var/www/html/artisan app:init diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100644 index 000000000..69a5a9d41 --- /dev/null +++ b/hooks/pre-commit @@ -0,0 +1,21 @@ +#!/bin/sh +# Detect whether /dev/tty is available & functional +if sh -c ": >/dev/tty" >/dev/null 2>/dev/null; then + exec < /dev/tty +fi + +# Get list of stashed PHP files +stashed_files=$(git diff --cached --name-only --diff-filter=ACM -- '*.php') + +# If there are no stashed PHP files, exit early +if [ -z "$stashed_files" ]; then + exit 0 +fi + +# Set files variable to only include stashed PHP files +files="$stashed_files" + +$(pwd)/vendor/bin/pint $files -q +if [ $? -eq 0 ]; then + git add $files +fi diff --git a/lang/ar.json b/lang/ar.json new file mode 100644 index 000000000..4b9afbe99 --- /dev/null +++ b/lang/ar.json @@ -0,0 +1,37 @@ +{ + "auth.login": "تسجيل الدخول", + "auth.login.azure": "تسجيل الدخول باستخدام Microsoft", + "auth.login.bitbucket": "تسجيل الدخول باستخدام Bitbucket", + "auth.login.github": "تسجيل الدخول باستخدام GitHub", + "auth.login.gitlab": "تسجيل الدخول باستخدام Gitlab", + "auth.login.google": "تسجيل الدخول باستخدام Google", + "auth.already_registered": "هل سبق لك التسجيل؟", + "auth.confirm_password": "تأكيد كلمة المرور", + "auth.forgot_password": "نسيت كلمة المرور", + "auth.forgot_password_send_email": "إرسال بريد إلكتروني لإعادة تعيين كلمة المرور", + "auth.register_now": "تسجيل", + "auth.logout": "تسجيل الخروج", + "auth.register": "تسجيل", + "auth.registration_disabled": "تم تعطيل التسجيل. يرجى التواصل مع المسؤول.", + "auth.reset_password": "إعادة تعيين كلمة المرور", + "auth.failed": "هذه البيانات لا تتطابق مع سجلاتنا.", + "auth.failed.callback": "فشل في معالجة استدعاء من مزود تسجيل الدخول.", + "auth.failed.password": "كلمة المرور المقدمة غير صحيحة.", + "auth.failed.email": "لا يمكننا العثور على مستخدم بهذا البريد الإلكتروني.", + "auth.throttle": "عدد محاولات تسجيل الدخول كثيرة جدًا. يرجى المحاولة مرة أخرى في :seconds ثانية.", + "input.name": "الاسم", + "input.email": "البريد الإلكتروني", + "input.password": "كلمة المرور", + "input.password.again": "كلمة المرور مرة أخرى", + "input.code": "الرمز لمرة واحدة", + "input.recovery_code": "رمز الاسترداد", + "button.save": "حفظ", + "repository.url": "أمثلة
للمستودعات العامة، استخدم https://....
للمستودعات الخاصة، استخدم git@....

سيتم تحديد الفرع main لـ https://github.com/coollabsio/coolify-examples
سيتم تحديد الفرع nodejs-fastify لـ https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify
سيتم تحديد الفرع main لـ https://gitea.com/sedlav/expressjs.git
سيتم تحديد الفرع main لـ https://gitlab.com/andrasbacsai/nodejs-example.git.", + "service.stop": "سيتم إيقاف هذه الخدمة.", + "resource.docker_cleanup": "قم بتشغيل Docker Cleanup (قم بإزالة الصور غير المستخدمة وذاكرة التخزين المؤقت للمنشئ).", + "resource.non_persistent": "سيتم حذف جميع البيانات غير الدائمة.", + "resource.delete_volumes": "حذف جميع المجلدات والملفات المرتبطة بهذا المورد بشكل دائم.", + "resource.delete_connected_networks": "حذف جميع الشبكات غير المحددة مسبقًا والمرتبطة بهذا المورد بشكل دائم.", + "resource.delete_configurations": "حذف جميع ملفات التعريف من الخادم بشكل دائم.", + "database.delete_backups_locally": "حذف كافة النسخ الاحتياطية نهائيًا من التخزين المحلي." +} diff --git a/lang/cs.json b/lang/cs.json new file mode 100644 index 000000000..48b47b06a --- /dev/null +++ b/lang/cs.json @@ -0,0 +1,30 @@ +{ + "auth.login": "Přihlásit se", + "auth.login.azure": "Přihlásit se pomocí Microsoftu", + "auth.login.bitbucket": "Přihlásit se pomocí Bitbucketu", + "auth.login.github": "Přihlásit se pomocí GitHubu", + "auth.login.gitlab": "Přihlásit se pomocí Gitlabu", + "auth.login.google": "Přihlásit se pomocí Google", + "auth.already_registered": "Již jste registrováni?", + "auth.confirm_password": "Potvrďte heslo", + "auth.forgot_password": "Zapomněli jste heslo", + "auth.forgot_password_send_email": "Poslat e-mail pro resetování hesla", + "auth.register_now": "Registrovat se", + "auth.logout": "Odhlásit se", + "auth.register": "Registrovat se", + "auth.registration_disabled": "Registrace je zakázána. Kontaktujte prosím administrátora.", + "auth.reset_password": "Obnovit heslo", + "auth.failed": "Tyto údaje neodpovídají našim záznamům.", + "auth.failed.callback": "Nepodařilo se zpracovat zpětné volání od poskytovatele přihlášení.", + "auth.failed.password": "Zadané heslo je nesprávné.", + "auth.failed.email": "Nemůžeme najít uživatele s touto e-mailovou adresou.", + "auth.throttle": "Příliš mnoho pokusů o přihlášení. Zkuste to prosím znovu za :seconds sekund.", + "input.name": "Jméno", + "input.email": "E-mail", + "input.password": "Heslo", + "input.password.again": "Heslo znovu", + "input.code": "Jednorázový kód", + "input.recovery_code": "Obnovovací kód", + "button.save": "Uložit", + "repository.url": "Příklady
Pro veřejné repozitáře, použijte https://....
Pro soukromé repozitáře, použijte git@....

https://github.com/coollabsio/coolify-examples main branch bude zvolena
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify branch bude vybrána.
https://gitea.com/sedlav/expressjs.git main branch vybrána.
https://gitlab.com/andrasbacsai/nodejs-example.git main branch bude vybrána." +} diff --git a/lang/de.json b/lang/de.json new file mode 100644 index 000000000..29fec629f --- /dev/null +++ b/lang/de.json @@ -0,0 +1,30 @@ +{ + "auth.login": "Anmelden", + "auth.login.azure": "Mit Microsoft anmelden", + "auth.login.bitbucket": "Mit Bitbucket anmelden", + "auth.login.github": "Mit GitHub anmelden", + "auth.login.gitlab": "Mit GitLab anmelden", + "auth.login.google": "Mit Google anmelden", + "auth.already_registered": "Bereits registriert?", + "auth.confirm_password": "Passwort bestätigen", + "auth.forgot_password": "Passwort vergessen", + "auth.forgot_password_send_email": "Passwort zurücksetzen E-Mail senden", + "auth.register_now": "Registrieren", + "auth.logout": "Abmelden", + "auth.register": "Registrieren", + "auth.registration_disabled": "Registrierung ist deaktiviert. Bitte kontaktiere einen Administrator.", + "auth.reset_password": "Passwort zurücksetzen", + "auth.failed": "Diese Anmeldedaten wurden nicht gefunden.", + "auth.failed.callback": "Fehlerhafte Verarbeitung der Antwort des Anmeldeanbieters.", + "auth.failed.password": "Das angegebene Passwort ist inkorrekt.", + "auth.failed.email": "Wir können keinen Benutzer mit dieser E-Mail Adresse finden.", + "auth.throttle": "Zu viele Anmeldeversuche. Bitte versuche es in :seconds Sekunden erneut.", + "input.name": "Name", + "input.email": "E-Mail", + "input.password": "Passwort", + "input.password.again": "Passwort wiederholen", + "input.code": "Einmalcode", + "input.recovery_code": "Wiederherstellungscode", + "button.save": "Speichern", + "repository.url": "Beispiele
Für öffentliche Repositories benutze https://....
Für private Repositories benutze git@....

https://github.com/coollabsio/coolify-examples main Branch wird ausgewählt
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify Branch wird ausgewählt.
https://gitea.com/sedlav/expressjs.git main Branch wird ausgewählt.
https://gitlab.com/andrasbacsai/nodejs-example.git main Branch wird ausgewählt." +} diff --git a/lang/en.json b/lang/en.json index 461a96e9a..5ea474b02 100644 --- a/lang/en.json +++ b/lang/en.json @@ -26,5 +26,13 @@ "input.code": "One-time code", "input.recovery_code": "Recovery code", "button.save": "Save", - "repository.url": "Examples
For Public repositories, use https://....
For Private repositories, use git@....

https://github.com/coollabsio/coolify-examples main branch will be selected
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify branch will be selected.
https://gitea.com/sedlav/expressjs.git main branch will be selected.
https://gitlab.com/andrasbacsai/nodejs-example.git main branch will be selected." + "repository.url": "Examples
For Public repositories, use https://....
For Private repositories, use git@....

https://github.com/coollabsio/coolify-examples main branch will be selected
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify branch will be selected.
https://gitea.com/sedlav/expressjs.git main branch will be selected.
https://gitlab.com/andrasbacsai/nodejs-example.git main branch will be selected.", + "service.stop": "This service will be stopped.", + "resource.docker_cleanup": "Run Docker Cleanup (remove unused images and builder cache).", + "resource.non_persistent": "All non-persistent data will be deleted.", + "resource.delete_volumes": "Permanently delete all volumes associated with this resource.", + "resource.delete_connected_networks": "Permanently delete all non-predefined networks associated with this resource.", + "resource.delete_configurations": "Permanently delete all configuration files from the server.", + "database.delete_backups_locally": "All backups will be permanently deleted from local storage.", + "warning.sslipdomain": "Your configuration is saved, but sslip domain with https is NOT recommended, because Let's Encrypt servers with this public domain are rate limited (SSL certificate validation will fail).

Use your own domain instead." } diff --git a/lang/es.json b/lang/es.json new file mode 100644 index 000000000..0d8c0c940 --- /dev/null +++ b/lang/es.json @@ -0,0 +1,30 @@ +{ + "auth.login": "Iniciar Sesión", + "auth.login.azure": "Acceder con Microsoft", + "auth.login.bitbucket": "Acceder con Bitbucket", + "auth.login.github": "Acceder con GitHub", + "auth.login.gitlab": "Acceder con Gitlab", + "auth.login.google": "Acceder con Google", + "auth.already_registered": "¿Ya estás registrado?", + "auth.confirm_password": "Confirmar contraseña", + "auth.forgot_password": "¿Olvidaste tu contraseña?", + "auth.forgot_password_send_email": "Enviar correo de recuperación de contraseña", + "auth.register_now": "Registrar", + "auth.logout": "Cerrar sesión", + "auth.register": "Registrar", + "auth.registration_disabled": "El registro está desactivado. Por favor contacta con el administrador.", + "auth.reset_password": "Cambiar contraseña", + "auth.failed": "Las credenciales no coinciden con nuestro registro..", + "auth.failed.callback": "Falló el proceso de inicio de sesión con el proveedor.", + "auth.failed.password": "La contraseña es incorrecta.", + "auth.failed.email": "No encontramos un usuario con ese correo.", + "auth.throttle": "Demasiados intentos. Por favor inténtalo en :seconds segundos.", + "input.name": "Nombre", + "input.email": "Correo", + "input.password": "Contraseña", + "input.password.again": "Escribe la contraseña otra vez", + "input.code": "Código de único uso", + "input.recovery_code": "Código de recuperación", + "button.save": "Guardar", + "repository.url": "Examples
Para repositorios públicos, usar https://....
Para repositorios privados, usar git@....

https://github.com/coollabsio/coolify-examples main la rama 'main' será seleccionada.
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify la rama 'nodejs-fastify' será seleccionada.
https://gitea.com/sedlav/expressjs.git main la rama 'main' será seleccionada.
https://gitlab.com/andrasbacsai/nodejs-example.git main la rama 'main' será seleccionada." +} \ No newline at end of file diff --git a/lang/fr.json b/lang/fr.json new file mode 100644 index 000000000..dbd5a1bf7 --- /dev/null +++ b/lang/fr.json @@ -0,0 +1,37 @@ +{ + "auth.login": "Connexion", + "auth.login.azure": "Connexion avec Microsoft", + "auth.login.bitbucket": "Connexion avec Bitbucket", + "auth.login.github": "Connexion avec GitHub", + "auth.login.gitlab": "Connexion avec Gitlab", + "auth.login.google": "Connexion avec Google", + "auth.already_registered": "Déjà enregistré ?", + "auth.confirm_password": "Confirmer le mot de passe", + "auth.forgot_password": "Mot de passe oublié", + "auth.forgot_password_send_email": "Envoyer l'email de réinitialisation de mot de passe", + "auth.register_now": "S'enregistrer", + "auth.logout": "Déconnexion", + "auth.register": "S'enregistrer", + "auth.registration_disabled": "L'enregistrement est désactivé. Merci de contacter l'administrateur.", + "auth.reset_password": "Réinitialiser le mot de passe", + "auth.failed": "Aucune correspondance n'a été trouvée pour les informations d'identification renseignées.", + "auth.failed.callback": "Erreur lors du processus de retour de la plateforme de connexion.", + "auth.failed.password": "Le mot de passe renseigné est incorrect.", + "auth.failed.email": "Aucun utilisateur avec cette adresse email n'a été trouvé.", + "auth.throttle": "Trop de tentatives de connexion. Merci de réessayer dans :seconds secondes.", + "input.name": "Nom", + "input.email": "Email", + "input.password": "Mot de passe", + "input.password.again": "Mot de passe identique", + "input.code": "Code à usage unique", + "input.recovery_code": "Code de récupération", + "button.save": "Sauvegarder", + "repository.url": "Exemples
Pour les dépôts publiques, utilisez https://....
Pour les dépôts privés, utilisez git@....

https://github.com/coollabsio/coolify-examples main sera la branche selectionnée
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify sera la branche selectionnée.
https://gitea.com/sedlav/expressjs.git main sera la branche selectionnée.
https://gitlab.com/andrasbacsai/nodejs-example.git main sera la branche selectionnée.", + "service.stop": "Ce service sera arrêté.", + "resource.docker_cleanup": "Exécuter le nettoyage Docker (supprimer les images inutilisées et le cache du builder).", + "resource.non_persistent": "Toutes les données non persistantes seront supprimées.", + "resource.delete_volumes": "Supprimer définitivement tous les volumes associés à cette ressource.", + "resource.delete_connected_networks": "Supprimer définitivement tous les réseaux non-prédéfinis associés à cette ressource.", + "resource.delete_configurations": "Supprimer définitivement tous les fichiers de configuration du serveur.", + "database.delete_backups_locally": "Toutes les sauvegardes seront définitivement supprimées du stockage local." +} diff --git a/lang/it.json b/lang/it.json new file mode 100644 index 000000000..6e4feb9cc --- /dev/null +++ b/lang/it.json @@ -0,0 +1,30 @@ +{ + "auth.login": "Accedi", + "auth.login.azure": "Accedi con Microsoft", + "auth.login.bitbucket": "Accedi con Bitbucket", + "auth.login.github": "Accedi con GitHub", + "auth.login.gitlab": "Accedi con Gitlab", + "auth.login.google": "Accedi con Google", + "auth.already_registered": "Già registrato?", + "auth.confirm_password": "Conferma password", + "auth.forgot_password": "Password dimenticata", + "auth.forgot_password_send_email": "Invia email per reimpostare la password", + "auth.register_now": "Registrati", + "auth.logout": "Esci", + "auth.register": "Registrati", + "auth.registration_disabled": "La registrazione è disabilitata. Si prega di contattare l'amministratore.", + "auth.reset_password": "Reimposta password", + "auth.failed": "Queste credenziali non corrispondono ai nostri record.", + "auth.failed.callback": "Errore durante l'elaborazione del callback dal provider di accesso.", + "auth.failed.password": "La password fornita non è corretta.", + "auth.failed.email": "Non possiamo trovare un utente con questo indirizzo email.", + "auth.throttle": "Troppi tentativi di accesso. Per favore riprova tra :seconds secondi.", + "input.name": "Nome", + "input.email": "Email", + "input.password": "Password", + "input.password.again": "Ripeti password", + "input.code": "Codice monouso", + "input.recovery_code": "Codice di recupero", + "button.save": "Salva", + "repository.url": "Esempi
Per i repository pubblici, utilizza https://....
Per i repository privati, utilizza git@....

https://github.com/coollabsio/coolify-examples verrà selezionato il branch main
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify verrà selezionato il branch nodejs-fastify.
https://gitea.com/sedlav/expressjs.git verrà selezionato il branch main.
https://gitlab.com/andrasbacsai/nodejs-example.git verrà selezionato il branch main." +} diff --git a/lang/ja.json b/lang/ja.json new file mode 100644 index 000000000..4652a3b17 --- /dev/null +++ b/lang/ja.json @@ -0,0 +1,30 @@ +{ + "auth.login": "ログイン", + "auth.login.azure": "Microsoftでログイン", + "auth.login.bitbucket": "Bitbucketでログイン", + "auth.login.github": "GitHubでログイン", + "auth.login.gitlab": "Gitlabでログイン", + "auth.login.google": "Googleでログイン", + "auth.already_registered": "すでに登録済みですか?", + "auth.confirm_password": "パスワードを確認", + "auth.forgot_password": "パスワードを忘れた", + "auth.forgot_password_send_email": "パスワードリセットメールを送信", + "auth.register_now": "今すぐ登録", + "auth.logout": "ログアウト", + "auth.register": "登録", + "auth.registration_disabled": "登録は無効です。管理者に連絡してください。", + "auth.reset_password": "パスワードをリセット", + "auth.failed": "これらの資格情報は記録と一致しません。", + "auth.failed.callback": "ログインプロバイダーからのコールバックの処理に失敗しました。", + "auth.failed.password": "提供されたパスワードが正しくありません。", + "auth.failed.email": "そのメールアドレスのユーザーが見つかりません。", + "auth.throttle": "ログイン試行回数が多すぎます。:seconds秒後にもう一度お試しください。", + "input.name": "名前", + "input.email": "メール", + "input.password": "パスワード", + "input.password.again": "パスワード再入力", + "input.code": "ワンタイムコード", + "input.recovery_code": "リカバリーコード", + "button.save": "保存", + "repository.url": "
公開リポジトリの場合はhttps://...を使用してください。
プライベートリポジトリの場合はgit@...を使用してください。

https://github.com/coollabsio/coolify-examples mainブランチが選択されます
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastifyブランチが選択されます。
https://gitea.com/sedlav/expressjs.git mainブランチが選択されます。
https://gitlab.com/andrasbacsai/nodejs-example.git mainブランチが選択されます。" +} diff --git a/lang/pt.json b/lang/pt.json new file mode 100644 index 000000000..b5dd5c434 --- /dev/null +++ b/lang/pt.json @@ -0,0 +1,30 @@ +{ + "auth.login": "Entrar", + "auth.login.azure": "Entrar com Microsoft", + "auth.login.bitbucket": "Entrar com Bitbucket", + "auth.login.github": "Entrar com GitHub", + "auth.login.gitlab": "Entrar com Gitlab", + "auth.login.google": "Entrar com Google", + "auth.already_registered": "Já tem uma conta?", + "auth.confirm_password": "Confirmar senha", + "auth.forgot_password": "Esqueceu a senha?", + "auth.forgot_password_send_email": "Enviar e-mail de redefinição de senha", + "auth.register_now": "Cadastrar-se", + "auth.logout": "Sair", + "auth.register": "Cadastrar", + "auth.registration_disabled": "Cadastro desativado. Por favor, entre em contato com o administrador.", + "auth.reset_password": "Redefinir senha", + "auth.failed": "Essas credenciais não correspondem aos nossos registros.", + "auth.failed.callback": "Falha ao processar o callback do provedor de login.", + "auth.failed.password": "A senha fornecida está incorreta.", + "auth.failed.email": "Não encontramos um usuário com esse endereço de e-mail.", + "auth.throttle": "Muitas tentativas de login. Por favor, tente novamente em :seconds segundos.", + "input.name": "Nome", + "input.email": "E-mail", + "input.password": "Senha", + "input.password.again": "Repetir senha", + "input.code": "Código único", + "input.recovery_code": "Código de recuperação", + "button.save": "Salvar", + "repository.url": "Exemplos
Para repositórios públicos, use https://....
Para repositórios privados, use git@....

https://github.com/coollabsio/coolify-examples a branch main será selecionada
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify a branch nodejs-fastify será selecionada.
https://gitea.com/sedlav/expressjs.git a branch main será selecionada.
https://gitlab.com/andrasbacsai/nodejs-example.git a branch main será selecionada." +} diff --git a/lang/ro.json b/lang/ro.json new file mode 100644 index 000000000..db1aa85db --- /dev/null +++ b/lang/ro.json @@ -0,0 +1,37 @@ +{ + "auth.login": "Autentificare", + "auth.login.azure": "Autentificare prin Microsoft", + "auth.login.bitbucket": "Autentificare prin Bitbucket", + "auth.login.github": "Autentificare prin GitHub", + "auth.login.gitlab": "Autentificare prin Gitlab", + "auth.login.google": "Autentificare prin Google", + "auth.already_registered": "Sunteți deja înregistrat?", + "auth.confirm_password": "Confirmați parola", + "auth.forgot_password": "Ați uitat parola", + "auth.forgot_password_send_email": "Trimiteți e-mail-ul pentru resetarea parolei", + "auth.register_now": "Înregistrare", + "auth.logout": "Deconectare", + "auth.register": "Înregistrare", + "auth.registration_disabled": "Înregistrarea este dezactivată. Vă rugăm să contactați administratorul site-ului.", + "auth.reset_password": "Resetare parolă", + "auth.failed": "Autentificare nereușită. Vă rugăm să verificați datele introduse.", + "auth.failed.callback": "A apărut o eroare în timpul autentificării cu furnizorul extern.", + "auth.failed.password": "Parola furnizată este incorectă.", + "auth.failed.email": "Nu putem găsi un utilizator cu această adresă de e-mail.", + "auth.throttle": "Prea multe încercări de autentificare. Vă rugăm să încercați din nou în :seconds secunde.", + "input.name": "Nume", + "input.email": "E-mail", + "input.password": "Parolă", + "input.password.again": "Repetați parola", + "input.code": "Cod de unică folosință", + "input.recovery_code": "Cod de recuperare", + "button.save": "Salvare", + "repository.url": "Exemple
Pentru depozite publice, utilizați https://....
Pentru depozite private, utilizați git@....

https://github.com/coollabsio/coolify-examples va fi selectată ramura main
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify va fi selectată ramura nodejs-fastify.
https://gitea.com/sedlav/expressjs.git va fi selectată ramura main.
https://gitlab.com/andrasbacsai/nodejs-example.git va fi selectată ramura main.", + "service.stop": "Acest serviciu va fi oprit.", + "resource.docker_cleanup": "Executați curățarea Docker (eliminați imaginile neutilizate și memoria cache a constructorului).", + "resource.non_persistent": "Toate datele nepersistente vor fi șterse.", + "resource.delete_volumes": "Ștergeți definitiv toate volumele asociate cu această resursă.", + "resource.delete_connected_networks": "Ștergeți definitiv toate rețelele non-predefinite asociate cu această resursă.", + "resource.delete_configurations": "Ștergeți definitiv toate fișierele de configurare de pe server.", + "database.delete_backups_locally": "Toate copiile de rezervă vor fi șterse definitiv din stocarea locală." +} diff --git a/lang/tr.json b/lang/tr.json new file mode 100644 index 000000000..255b0d15b --- /dev/null +++ b/lang/tr.json @@ -0,0 +1,30 @@ +{ + "auth.login": "Giriş", + "auth.login.azure": "Microsoft ile Giriş Yap", + "auth.login.bitbucket": "Bitbucket ile Giriş Yap", + "auth.login.github": "GitHub ile Giriş Yap", + "auth.login.gitlab": "GitLab ile Giriş Yap", + "auth.login.google": "Google ile Giriş Yap", + "auth.already_registered": "Zaten kayıtlı mısınız?", + "auth.confirm_password": "Şifreyi Onayla", + "auth.forgot_password": "Şifremi Unuttum", + "auth.forgot_password_send_email": "Şifre sıfırlama e-postası gönder", + "auth.register_now": "Kayıt Ol", + "auth.logout": "Çıkış Yap", + "auth.register": "Kayıt Ol", + "auth.registration_disabled": "Kayıt devre dışı bırakıldı. Lütfen yöneticiyle iletişime geçin.", + "auth.reset_password": "Şifreyi Sıfırla", + "auth.failed": "Bu kimlik bilgileri kayıtlarımızla eşleşmiyor.", + "auth.failed.callback": "Giriş sağlayıcıdan gelen istek işlenemedi.", + "auth.failed.password": "Sağlanan şifre yanlış.", + "auth.failed.email": "Bu e-posta adresiyle bir kullanıcı bulamıyoruz.", + "auth.throttle": "Çok fazla giriş denemesi. Lütfen :seconds saniye sonra tekrar deneyin.", + "input.name": "İsim", + "input.email": "E-posta", + "input.password": "Şifre", + "input.password.again": "Şifreyi Tekrar Girin", + "input.code": "Tek Kullanımlık Kod", + "input.recovery_code": "Kurtarma Kodu", + "button.save": "Kaydet", + "repository.url": "Örnekler
Halka açık depolar için https://... kullanın.
Özel depolar için git@... kullanın.

https://github.com/coollabsio/coolify-examples main dalı seçilecek
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify dalı seçilecek.
https://gitea.com/sedlav/expressjs.git main dalı seçilecek.
https://gitlab.com/andrasbacsai/nodejs-example.git main dalı seçilecek." +} diff --git a/lang/vi.json b/lang/vi.json new file mode 100644 index 000000000..548dbe8b7 --- /dev/null +++ b/lang/vi.json @@ -0,0 +1,30 @@ +{ + "auth.login": "Đăng Nhập", + "auth.login.azure": "Đăng Nhập Bằng Microsoft", + "auth.login.bitbucket": "Đăng Nhập Bằng Bitbucket", + "auth.login.github": "Đăng Nhập Bằng GitHub", + "auth.login.gitlab": "Đăng Nhập Bằng Gitlab", + "auth.login.google": "Đăng Nhập Bằng Google", + "auth.already_registered": "Đã đăng ký?", + "auth.confirm_password": "Nhập lại mật khẩu", + "auth.forgot_password": "Quên mật khẩu", + "auth.forgot_password_send_email": "Gửi email đặt lại mật khẩu", + "auth.register_now": "Đăng ký ngay", + "auth.logout": "Đăng xuất", + "auth.register": "Đăng ký", + "auth.registration_disabled": "Đăng ký không khả dụng. Vui lòng liên hệ quản trị viên.", + "auth.reset_password": "Đặt lại mật khẩu", + "auth.failed": "Thông tin đăng nhập không khớp với bất kỳ tài khoản nào.", + "auth.failed.callback": "Xử lý thông tin từ nhà cung cấp đăng nhập thất bại.", + "auth.failed.password": "Mật khẩu bạn cung cấp không chính xác.", + "auth.failed.email": "Không có người dùng nào đã đăng ký với email đó.", + "auth.throttle": "Quá nhiều lần đăng nhập thất bại. Vui lòng thử lại sau :seconds giây.", + "input.name": "Tên", + "input.email": "Email", + "input.password": "Mật khẩu", + "input.password.again": "Mật khẩu lần nữa", + "input.code": "One-time code", + "input.recovery_code": "Mã khôi phục", + "button.save": "Lưu", + "repository.url": "Ví dụ
Với repo công khai, sử dụng https://....
Với repo riêng tư, sử dụng git@....

https://github.com/coollabsio/coolify-examples nhánh chính sẽ được chọn
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nhánh nodejs-fastify sẽ được chọn.
https://gitea.com/sedlav/expressjs.git nhánh chính sẽ được chọn.
https://gitlab.com/andrasbacsai/nodejs-example.git nhánh chính sẽ được chọn." +} diff --git a/lang/zh-tw.json b/lang/zh-tw.json new file mode 100644 index 000000000..63956f7a1 --- /dev/null +++ b/lang/zh-tw.json @@ -0,0 +1,30 @@ +{ + "auth.login": "登入", + "auth.login.azure": "使用 Microsoft 登入", + "auth.login.bitbucket": "使用 Bitbucket 登入", + "auth.login.github": "使用 GitHub 登入", + "auth.login.gitlab": "使用 Gitlab 登入", + "auth.login.google": "使用 Google 登入", + "auth.already_registered": "已經註冊?", + "auth.confirm_password": "確認密碼", + "auth.forgot_password": "忘記密碼", + "auth.forgot_password_send_email": "發送重設密碼電郵", + "auth.register_now": "註冊", + "auth.logout": "登出", + "auth.register": "註冊", + "auth.registration_disabled": "註冊已停用,請聯絡管理員。", + "auth.reset_password": "重設密碼", + "auth.failed": "這些憑證與我們的記錄不符。", + "auth.failed.callback": "無法處理來自登入提供者的回呼。", + "auth.failed.password": "密碼錯誤。", + "auth.failed.email": "找不到該電子郵件地址的使用者。", + "auth.throttle": "登入嘗試次數太多。請在 :seconds 秒後重試。", + "input.name": "名稱", + "input.email": "電子郵件", + "input.password": "密碼", + "input.password.again": "再次輸入密碼", + "input.code": "一次性代碼", + "input.recovery_code": "恢復碼", + "button.save": "儲存", + "repository.url": "例子
對於公共代碼倉庫,請使用 https://...
對於私有代碼倉庫,請使用 git@...

https://github.com/coollabsio/coolify-examples main 分支將被選擇
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify 分支將被選擇。
https://gitea.com/sedlav/expressjs.git main 分支將被選擇。
https://gitlab.com/andrasbacsai/nodejs-example.git main 分支將被選擇。" +} diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 000000000..f5abefa1e --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,5322 @@ +openapi: 3.0.0 +info: + title: Coolify + version: '0.1' +servers: + - + url: 'https://app.coolify.io/api/v1' + description: 'Coolify Cloud API. Change the host to your own instance if you are self-hosting.' +paths: + /applications: + get: + tags: + - Applications + summary: List + description: 'List all applications.' + operationId: list-applications + responses: + '200': + description: 'Get all applications.' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Application' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + /applications/public: + post: + tags: + - Applications + summary: 'Create (Public)' + description: 'Create new application based on a public git repository.' + operationId: create-public-application + requestBody: + description: 'Application object that needs to be created.' + required: true + content: + application/json: + schema: + required: + - project_uuid + - server_uuid + - environment_name + - git_repository + - git_branch + - build_pack + - ports_exposes + properties: + project_uuid: + type: string + description: 'The project UUID.' + server_uuid: + type: string + description: 'The server UUID.' + environment_name: + type: string + description: 'The environment name.' + git_repository: + type: string + description: 'The git repository URL.' + git_branch: + type: string + description: 'The git branch.' + build_pack: + type: string + enum: [nixpacks, static, dockerfile, dockercompose] + description: 'The build pack type.' + ports_exposes: + type: string + description: 'The ports to expose.' + destination_uuid: + type: string + description: 'The destination UUID.' + name: + type: string + description: 'The application name.' + description: + type: string + description: 'The application description.' + domains: + type: string + description: 'The application domains.' + git_commit_sha: + type: string + description: 'The git commit SHA.' + docker_registry_image_name: + type: string + description: 'The docker registry image name.' + docker_registry_image_tag: + type: string + description: 'The docker registry image tag.' + is_static: + type: boolean + description: 'The flag to indicate if the application is static.' + static_image: + type: string + enum: ['nginx:alpine'] + description: 'The static image.' + install_command: + type: string + description: 'The install command.' + build_command: + type: string + description: 'The build command.' + start_command: + type: string + description: 'The start command.' + ports_mappings: + type: string + description: 'The ports mappings.' + base_directory: + type: string + description: 'The base directory for all commands.' + publish_directory: + type: string + description: 'The publish directory.' + health_check_enabled: + type: boolean + description: 'Health check enabled.' + health_check_path: + type: string + description: 'Health check path.' + health_check_port: + type: string + nullable: true + description: 'Health check port.' + health_check_host: + type: string + nullable: true + description: 'Health check host.' + health_check_method: + type: string + description: 'Health check method.' + health_check_return_code: + type: integer + description: 'Health check return code.' + health_check_scheme: + type: string + description: 'Health check scheme.' + health_check_response_text: + type: string + nullable: true + description: 'Health check response text.' + health_check_interval: + type: integer + description: 'Health check interval in seconds.' + health_check_timeout: + type: integer + description: 'Health check timeout in seconds.' + health_check_retries: + type: integer + description: 'Health check retries count.' + health_check_start_period: + type: integer + description: 'Health check start period in seconds.' + limits_memory: + type: string + description: 'Memory limit.' + limits_memory_swap: + type: string + description: 'Memory swap limit.' + limits_memory_swappiness: + type: integer + description: 'Memory swappiness.' + limits_memory_reservation: + type: string + description: 'Memory reservation.' + limits_cpus: + type: string + description: 'CPU limit.' + limits_cpuset: + type: string + nullable: true + description: 'CPU set.' + limits_cpu_shares: + type: integer + description: 'CPU shares.' + custom_labels: + type: string + description: 'Custom labels.' + custom_docker_run_options: + type: string + description: 'Custom docker run options.' + post_deployment_command: + type: string + description: 'Post deployment command.' + post_deployment_command_container: + type: string + description: 'Post deployment command container.' + pre_deployment_command: + type: string + description: 'Pre deployment command.' + pre_deployment_command_container: + type: string + description: 'Pre deployment command container.' + manual_webhook_secret_github: + type: string + description: 'Manual webhook secret for Github.' + manual_webhook_secret_gitlab: + type: string + description: 'Manual webhook secret for Gitlab.' + manual_webhook_secret_bitbucket: + type: string + description: 'Manual webhook secret for Bitbucket.' + manual_webhook_secret_gitea: + type: string + description: 'Manual webhook secret for Gitea.' + redirect: + type: string + nullable: true + description: 'How to set redirect with Traefik / Caddy. www<->non-www.' + enum: [www, non-www, both] + instant_deploy: + type: boolean + description: 'The flag to indicate if the application should be deployed instantly.' + dockerfile: + type: string + description: 'The Dockerfile content.' + docker_compose_location: + type: string + description: 'The Docker Compose location.' + docker_compose_raw: + type: string + description: 'The Docker Compose raw content.' + docker_compose_custom_start_command: + type: string + description: 'The Docker Compose custom start command.' + docker_compose_custom_build_command: + type: string + description: 'The Docker Compose custom build command.' + docker_compose_domains: + type: array + description: 'The Docker Compose domains.' + watch_paths: + type: string + description: 'The watch paths.' + use_build_server: + type: boolean + nullable: true + description: 'Use build server.' + type: object + responses: + '200': + description: 'Application created successfully.' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + /applications/private-github-app: + post: + tags: + - Applications + summary: 'Create (Private - GH App)' + description: 'Create new application based on a private repository through a Github App.' + operationId: create-private-github-app-application + requestBody: + description: 'Application object that needs to be created.' + required: true + content: + application/json: + schema: + required: + - project_uuid + - server_uuid + - environment_name + - github_app_uuid + - git_repository + - git_branch + - build_pack + - ports_exposes + properties: + project_uuid: + type: string + description: 'The project UUID.' + server_uuid: + type: string + description: 'The server UUID.' + environment_name: + type: string + description: 'The environment name.' + github_app_uuid: + type: string + description: 'The Github App UUID.' + git_repository: + type: string + description: 'The git repository URL.' + git_branch: + type: string + description: 'The git branch.' + ports_exposes: + type: string + description: 'The ports to expose.' + destination_uuid: + type: string + description: 'The destination UUID.' + build_pack: + type: string + enum: [nixpacks, static, dockerfile, dockercompose] + description: 'The build pack type.' + name: + type: string + description: 'The application name.' + description: + type: string + description: 'The application description.' + domains: + type: string + description: 'The application domains.' + git_commit_sha: + type: string + description: 'The git commit SHA.' + docker_registry_image_name: + type: string + description: 'The docker registry image name.' + docker_registry_image_tag: + type: string + description: 'The docker registry image tag.' + is_static: + type: boolean + description: 'The flag to indicate if the application is static.' + static_image: + type: string + enum: ['nginx:alpine'] + description: 'The static image.' + install_command: + type: string + description: 'The install command.' + build_command: + type: string + description: 'The build command.' + start_command: + type: string + description: 'The start command.' + ports_mappings: + type: string + description: 'The ports mappings.' + base_directory: + type: string + description: 'The base directory for all commands.' + publish_directory: + type: string + description: 'The publish directory.' + health_check_enabled: + type: boolean + description: 'Health check enabled.' + health_check_path: + type: string + description: 'Health check path.' + health_check_port: + type: string + nullable: true + description: 'Health check port.' + health_check_host: + type: string + nullable: true + description: 'Health check host.' + health_check_method: + type: string + description: 'Health check method.' + health_check_return_code: + type: integer + description: 'Health check return code.' + health_check_scheme: + type: string + description: 'Health check scheme.' + health_check_response_text: + type: string + nullable: true + description: 'Health check response text.' + health_check_interval: + type: integer + description: 'Health check interval in seconds.' + health_check_timeout: + type: integer + description: 'Health check timeout in seconds.' + health_check_retries: + type: integer + description: 'Health check retries count.' + health_check_start_period: + type: integer + description: 'Health check start period in seconds.' + limits_memory: + type: string + description: 'Memory limit.' + limits_memory_swap: + type: string + description: 'Memory swap limit.' + limits_memory_swappiness: + type: integer + description: 'Memory swappiness.' + limits_memory_reservation: + type: string + description: 'Memory reservation.' + limits_cpus: + type: string + description: 'CPU limit.' + limits_cpuset: + type: string + nullable: true + description: 'CPU set.' + limits_cpu_shares: + type: integer + description: 'CPU shares.' + custom_labels: + type: string + description: 'Custom labels.' + custom_docker_run_options: + type: string + description: 'Custom docker run options.' + post_deployment_command: + type: string + description: 'Post deployment command.' + post_deployment_command_container: + type: string + description: 'Post deployment command container.' + pre_deployment_command: + type: string + description: 'Pre deployment command.' + pre_deployment_command_container: + type: string + description: 'Pre deployment command container.' + manual_webhook_secret_github: + type: string + description: 'Manual webhook secret for Github.' + manual_webhook_secret_gitlab: + type: string + description: 'Manual webhook secret for Gitlab.' + manual_webhook_secret_bitbucket: + type: string + description: 'Manual webhook secret for Bitbucket.' + manual_webhook_secret_gitea: + type: string + description: 'Manual webhook secret for Gitea.' + redirect: + type: string + nullable: true + description: 'How to set redirect with Traefik / Caddy. www<->non-www.' + enum: [www, non-www, both] + instant_deploy: + type: boolean + description: 'The flag to indicate if the application should be deployed instantly.' + dockerfile: + type: string + description: 'The Dockerfile content.' + docker_compose_location: + type: string + description: 'The Docker Compose location.' + docker_compose_raw: + type: string + description: 'The Docker Compose raw content.' + docker_compose_custom_start_command: + type: string + description: 'The Docker Compose custom start command.' + docker_compose_custom_build_command: + type: string + description: 'The Docker Compose custom build command.' + docker_compose_domains: + type: array + description: 'The Docker Compose domains.' + watch_paths: + type: string + description: 'The watch paths.' + use_build_server: + type: boolean + nullable: true + description: 'Use build server.' + type: object + responses: + '200': + description: 'Application created successfully.' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + /applications/private-deploy-key: + post: + tags: + - Applications + summary: 'Create (Private - Deploy Key)' + description: 'Create new application based on a private repository through a Deploy Key.' + operationId: create-private-deploy-key-application + requestBody: + description: 'Application object that needs to be created.' + required: true + content: + application/json: + schema: + required: + - project_uuid + - server_uuid + - environment_name + - private_key_uuid + - git_repository + - git_branch + - build_pack + - ports_exposes + properties: + project_uuid: + type: string + description: 'The project UUID.' + server_uuid: + type: string + description: 'The server UUID.' + environment_name: + type: string + description: 'The environment name.' + private_key_uuid: + type: string + description: 'The private key UUID.' + git_repository: + type: string + description: 'The git repository URL.' + git_branch: + type: string + description: 'The git branch.' + ports_exposes: + type: string + description: 'The ports to expose.' + destination_uuid: + type: string + description: 'The destination UUID.' + build_pack: + type: string + enum: [nixpacks, static, dockerfile, dockercompose] + description: 'The build pack type.' + name: + type: string + description: 'The application name.' + description: + type: string + description: 'The application description.' + domains: + type: string + description: 'The application domains.' + git_commit_sha: + type: string + description: 'The git commit SHA.' + docker_registry_image_name: + type: string + description: 'The docker registry image name.' + docker_registry_image_tag: + type: string + description: 'The docker registry image tag.' + is_static: + type: boolean + description: 'The flag to indicate if the application is static.' + static_image: + type: string + enum: ['nginx:alpine'] + description: 'The static image.' + install_command: + type: string + description: 'The install command.' + build_command: + type: string + description: 'The build command.' + start_command: + type: string + description: 'The start command.' + ports_mappings: + type: string + description: 'The ports mappings.' + base_directory: + type: string + description: 'The base directory for all commands.' + publish_directory: + type: string + description: 'The publish directory.' + health_check_enabled: + type: boolean + description: 'Health check enabled.' + health_check_path: + type: string + description: 'Health check path.' + health_check_port: + type: string + nullable: true + description: 'Health check port.' + health_check_host: + type: string + nullable: true + description: 'Health check host.' + health_check_method: + type: string + description: 'Health check method.' + health_check_return_code: + type: integer + description: 'Health check return code.' + health_check_scheme: + type: string + description: 'Health check scheme.' + health_check_response_text: + type: string + nullable: true + description: 'Health check response text.' + health_check_interval: + type: integer + description: 'Health check interval in seconds.' + health_check_timeout: + type: integer + description: 'Health check timeout in seconds.' + health_check_retries: + type: integer + description: 'Health check retries count.' + health_check_start_period: + type: integer + description: 'Health check start period in seconds.' + limits_memory: + type: string + description: 'Memory limit.' + limits_memory_swap: + type: string + description: 'Memory swap limit.' + limits_memory_swappiness: + type: integer + description: 'Memory swappiness.' + limits_memory_reservation: + type: string + description: 'Memory reservation.' + limits_cpus: + type: string + description: 'CPU limit.' + limits_cpuset: + type: string + nullable: true + description: 'CPU set.' + limits_cpu_shares: + type: integer + description: 'CPU shares.' + custom_labels: + type: string + description: 'Custom labels.' + custom_docker_run_options: + type: string + description: 'Custom docker run options.' + post_deployment_command: + type: string + description: 'Post deployment command.' + post_deployment_command_container: + type: string + description: 'Post deployment command container.' + pre_deployment_command: + type: string + description: 'Pre deployment command.' + pre_deployment_command_container: + type: string + description: 'Pre deployment command container.' + manual_webhook_secret_github: + type: string + description: 'Manual webhook secret for Github.' + manual_webhook_secret_gitlab: + type: string + description: 'Manual webhook secret for Gitlab.' + manual_webhook_secret_bitbucket: + type: string + description: 'Manual webhook secret for Bitbucket.' + manual_webhook_secret_gitea: + type: string + description: 'Manual webhook secret for Gitea.' + redirect: + type: string + nullable: true + description: 'How to set redirect with Traefik / Caddy. www<->non-www.' + enum: [www, non-www, both] + instant_deploy: + type: boolean + description: 'The flag to indicate if the application should be deployed instantly.' + dockerfile: + type: string + description: 'The Dockerfile content.' + docker_compose_location: + type: string + description: 'The Docker Compose location.' + docker_compose_raw: + type: string + description: 'The Docker Compose raw content.' + docker_compose_custom_start_command: + type: string + description: 'The Docker Compose custom start command.' + docker_compose_custom_build_command: + type: string + description: 'The Docker Compose custom build command.' + docker_compose_domains: + type: array + description: 'The Docker Compose domains.' + watch_paths: + type: string + description: 'The watch paths.' + use_build_server: + type: boolean + nullable: true + description: 'Use build server.' + type: object + responses: + '200': + description: 'Application created successfully.' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + /applications/dockerfile: + post: + tags: + - Applications + summary: 'Create (Dockerfile)' + description: 'Create new application based on a simple Dockerfile.' + operationId: create-dockerfile-application + requestBody: + description: 'Application object that needs to be created.' + required: true + content: + application/json: + schema: + required: + - project_uuid + - server_uuid + - environment_name + - dockerfile + properties: + project_uuid: + type: string + description: 'The project UUID.' + server_uuid: + type: string + description: 'The server UUID.' + environment_name: + type: string + description: 'The environment name.' + dockerfile: + type: string + description: 'The Dockerfile content.' + build_pack: + type: string + enum: [nixpacks, static, dockerfile, dockercompose] + description: 'The build pack type.' + ports_exposes: + type: string + description: 'The ports to expose.' + destination_uuid: + type: string + description: 'The destination UUID.' + name: + type: string + description: 'The application name.' + description: + type: string + description: 'The application description.' + domains: + type: string + description: 'The application domains.' + docker_registry_image_name: + type: string + description: 'The docker registry image name.' + docker_registry_image_tag: + type: string + description: 'The docker registry image tag.' + ports_mappings: + type: string + description: 'The ports mappings.' + base_directory: + type: string + description: 'The base directory for all commands.' + health_check_enabled: + type: boolean + description: 'Health check enabled.' + health_check_path: + type: string + description: 'Health check path.' + health_check_port: + type: string + nullable: true + description: 'Health check port.' + health_check_host: + type: string + nullable: true + description: 'Health check host.' + health_check_method: + type: string + description: 'Health check method.' + health_check_return_code: + type: integer + description: 'Health check return code.' + health_check_scheme: + type: string + description: 'Health check scheme.' + health_check_response_text: + type: string + nullable: true + description: 'Health check response text.' + health_check_interval: + type: integer + description: 'Health check interval in seconds.' + health_check_timeout: + type: integer + description: 'Health check timeout in seconds.' + health_check_retries: + type: integer + description: 'Health check retries count.' + health_check_start_period: + type: integer + description: 'Health check start period in seconds.' + limits_memory: + type: string + description: 'Memory limit.' + limits_memory_swap: + type: string + description: 'Memory swap limit.' + limits_memory_swappiness: + type: integer + description: 'Memory swappiness.' + limits_memory_reservation: + type: string + description: 'Memory reservation.' + limits_cpus: + type: string + description: 'CPU limit.' + limits_cpuset: + type: string + nullable: true + description: 'CPU set.' + limits_cpu_shares: + type: integer + description: 'CPU shares.' + custom_labels: + type: string + description: 'Custom labels.' + custom_docker_run_options: + type: string + description: 'Custom docker run options.' + post_deployment_command: + type: string + description: 'Post deployment command.' + post_deployment_command_container: + type: string + description: 'Post deployment command container.' + pre_deployment_command: + type: string + description: 'Pre deployment command.' + pre_deployment_command_container: + type: string + description: 'Pre deployment command container.' + manual_webhook_secret_github: + type: string + description: 'Manual webhook secret for Github.' + manual_webhook_secret_gitlab: + type: string + description: 'Manual webhook secret for Gitlab.' + manual_webhook_secret_bitbucket: + type: string + description: 'Manual webhook secret for Bitbucket.' + manual_webhook_secret_gitea: + type: string + description: 'Manual webhook secret for Gitea.' + redirect: + type: string + nullable: true + description: 'How to set redirect with Traefik / Caddy. www<->non-www.' + enum: [www, non-www, both] + instant_deploy: + type: boolean + description: 'The flag to indicate if the application should be deployed instantly.' + use_build_server: + type: boolean + nullable: true + description: 'Use build server.' + type: object + responses: + '200': + description: 'Application created successfully.' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + /applications/dockerimage: + post: + tags: + - Applications + summary: 'Create (Docker Image)' + description: 'Create new application based on a prebuilt docker image' + operationId: create-dockerimage-application + requestBody: + description: 'Application object that needs to be created.' + required: true + content: + application/json: + schema: + required: + - project_uuid + - server_uuid + - environment_name + - docker_registry_image_name + - ports_exposes + properties: + project_uuid: + type: string + description: 'The project UUID.' + server_uuid: + type: string + description: 'The server UUID.' + environment_name: + type: string + description: 'The environment name.' + docker_registry_image_name: + type: string + description: 'The docker registry image name.' + docker_registry_image_tag: + type: string + description: 'The docker registry image tag.' + ports_exposes: + type: string + description: 'The ports to expose.' + destination_uuid: + type: string + description: 'The destination UUID.' + name: + type: string + description: 'The application name.' + description: + type: string + description: 'The application description.' + domains: + type: string + description: 'The application domains.' + ports_mappings: + type: string + description: 'The ports mappings.' + health_check_enabled: + type: boolean + description: 'Health check enabled.' + health_check_path: + type: string + description: 'Health check path.' + health_check_port: + type: string + nullable: true + description: 'Health check port.' + health_check_host: + type: string + nullable: true + description: 'Health check host.' + health_check_method: + type: string + description: 'Health check method.' + health_check_return_code: + type: integer + description: 'Health check return code.' + health_check_scheme: + type: string + description: 'Health check scheme.' + health_check_response_text: + type: string + nullable: true + description: 'Health check response text.' + health_check_interval: + type: integer + description: 'Health check interval in seconds.' + health_check_timeout: + type: integer + description: 'Health check timeout in seconds.' + health_check_retries: + type: integer + description: 'Health check retries count.' + health_check_start_period: + type: integer + description: 'Health check start period in seconds.' + limits_memory: + type: string + description: 'Memory limit.' + limits_memory_swap: + type: string + description: 'Memory swap limit.' + limits_memory_swappiness: + type: integer + description: 'Memory swappiness.' + limits_memory_reservation: + type: string + description: 'Memory reservation.' + limits_cpus: + type: string + description: 'CPU limit.' + limits_cpuset: + type: string + nullable: true + description: 'CPU set.' + limits_cpu_shares: + type: integer + description: 'CPU shares.' + custom_labels: + type: string + description: 'Custom labels.' + custom_docker_run_options: + type: string + description: 'Custom docker run options.' + post_deployment_command: + type: string + description: 'Post deployment command.' + post_deployment_command_container: + type: string + description: 'Post deployment command container.' + pre_deployment_command: + type: string + description: 'Pre deployment command.' + pre_deployment_command_container: + type: string + description: 'Pre deployment command container.' + manual_webhook_secret_github: + type: string + description: 'Manual webhook secret for Github.' + manual_webhook_secret_gitlab: + type: string + description: 'Manual webhook secret for Gitlab.' + manual_webhook_secret_bitbucket: + type: string + description: 'Manual webhook secret for Bitbucket.' + manual_webhook_secret_gitea: + type: string + description: 'Manual webhook secret for Gitea.' + redirect: + type: string + nullable: true + description: 'How to set redirect with Traefik / Caddy. www<->non-www.' + enum: [www, non-www, both] + instant_deploy: + type: boolean + description: 'The flag to indicate if the application should be deployed instantly.' + use_build_server: + type: boolean + nullable: true + description: 'Use build server.' + type: object + responses: + '200': + description: 'Application created successfully.' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + /applications/dockercompose: + post: + tags: + - Applications + summary: 'Create (Docker Compose)' + description: 'Create new application based on a docker-compose file.' + operationId: create-dockercompose-application + requestBody: + description: 'Application object that needs to be created.' + required: true + content: + application/json: + schema: + required: + - project_uuid + - server_uuid + - environment_name + - docker_compose_raw + properties: + project_uuid: + type: string + description: 'The project UUID.' + server_uuid: + type: string + description: 'The server UUID.' + environment_name: + type: string + description: 'The environment name.' + docker_compose_raw: + type: string + description: 'The Docker Compose raw content.' + destination_uuid: + type: string + description: 'The destination UUID if the server has more than one destinations.' + name: + type: string + description: 'The application name.' + description: + type: string + description: 'The application description.' + instant_deploy: + type: boolean + description: 'The flag to indicate if the application should be deployed instantly.' + use_build_server: + type: boolean + nullable: true + description: 'Use build server.' + type: object + responses: + '200': + description: 'Application created successfully.' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + '/applications/{uuid}': + get: + tags: + - Applications + summary: Get + description: 'Get application by UUID.' + operationId: get-application-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the application.' + required: true + schema: + type: string + format: uuid + responses: + '200': + description: 'Get application by UUID.' + content: + application/json: + schema: + $ref: '#/components/schemas/Application' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + delete: + tags: + - Applications + summary: Delete + description: 'Delete application by UUID.' + operationId: delete-application-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the application.' + required: true + schema: + type: string + format: uuid + - + name: delete_configurations + in: query + description: 'Delete configurations.' + required: false + schema: + type: boolean + default: true + - + name: delete_volumes + in: query + description: 'Delete volumes.' + required: false + schema: + type: boolean + default: true + - + name: docker_cleanup + in: query + description: 'Run docker cleanup.' + required: false + schema: + type: boolean + default: true + - + name: delete_connected_networks + in: query + description: 'Delete connected networks.' + required: false + schema: + type: boolean + default: true + responses: + '200': + description: 'Application deleted.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Application deleted.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + patch: + tags: + - Applications + summary: Update + description: 'Update application by UUID.' + operationId: update-application-by-uuid + requestBody: + description: 'Application updated.' + required: true + content: + application/json: + schema: + properties: + project_uuid: + type: string + description: 'The project UUID.' + server_uuid: + type: string + description: 'The server UUID.' + environment_name: + type: string + description: 'The environment name.' + github_app_uuid: + type: string + description: 'The Github App UUID.' + git_repository: + type: string + description: 'The git repository URL.' + git_branch: + type: string + description: 'The git branch.' + ports_exposes: + type: string + description: 'The ports to expose.' + destination_uuid: + type: string + description: 'The destination UUID.' + build_pack: + type: string + enum: [nixpacks, static, dockerfile, dockercompose] + description: 'The build pack type.' + name: + type: string + description: 'The application name.' + description: + type: string + description: 'The application description.' + domains: + type: string + description: 'The application domains.' + git_commit_sha: + type: string + description: 'The git commit SHA.' + docker_registry_image_name: + type: string + description: 'The docker registry image name.' + docker_registry_image_tag: + type: string + description: 'The docker registry image tag.' + is_static: + type: boolean + description: 'The flag to indicate if the application is static.' + install_command: + type: string + description: 'The install command.' + build_command: + type: string + description: 'The build command.' + start_command: + type: string + description: 'The start command.' + ports_mappings: + type: string + description: 'The ports mappings.' + base_directory: + type: string + description: 'The base directory for all commands.' + publish_directory: + type: string + description: 'The publish directory.' + health_check_enabled: + type: boolean + description: 'Health check enabled.' + health_check_path: + type: string + description: 'Health check path.' + health_check_port: + type: string + nullable: true + description: 'Health check port.' + health_check_host: + type: string + nullable: true + description: 'Health check host.' + health_check_method: + type: string + description: 'Health check method.' + health_check_return_code: + type: integer + description: 'Health check return code.' + health_check_scheme: + type: string + description: 'Health check scheme.' + health_check_response_text: + type: string + nullable: true + description: 'Health check response text.' + health_check_interval: + type: integer + description: 'Health check interval in seconds.' + health_check_timeout: + type: integer + description: 'Health check timeout in seconds.' + health_check_retries: + type: integer + description: 'Health check retries count.' + health_check_start_period: + type: integer + description: 'Health check start period in seconds.' + limits_memory: + type: string + description: 'Memory limit.' + limits_memory_swap: + type: string + description: 'Memory swap limit.' + limits_memory_swappiness: + type: integer + description: 'Memory swappiness.' + limits_memory_reservation: + type: string + description: 'Memory reservation.' + limits_cpus: + type: string + description: 'CPU limit.' + limits_cpuset: + type: string + nullable: true + description: 'CPU set.' + limits_cpu_shares: + type: integer + description: 'CPU shares.' + custom_labels: + type: string + description: 'Custom labels.' + custom_docker_run_options: + type: string + description: 'Custom docker run options.' + post_deployment_command: + type: string + description: 'Post deployment command.' + post_deployment_command_container: + type: string + description: 'Post deployment command container.' + pre_deployment_command: + type: string + description: 'Pre deployment command.' + pre_deployment_command_container: + type: string + description: 'Pre deployment command container.' + manual_webhook_secret_github: + type: string + description: 'Manual webhook secret for Github.' + manual_webhook_secret_gitlab: + type: string + description: 'Manual webhook secret for Gitlab.' + manual_webhook_secret_bitbucket: + type: string + description: 'Manual webhook secret for Bitbucket.' + manual_webhook_secret_gitea: + type: string + description: 'Manual webhook secret for Gitea.' + redirect: + type: string + nullable: true + description: 'How to set redirect with Traefik / Caddy. www<->non-www.' + enum: [www, non-www, both] + instant_deploy: + type: boolean + description: 'The flag to indicate if the application should be deployed instantly.' + dockerfile: + type: string + description: 'The Dockerfile content.' + docker_compose_location: + type: string + description: 'The Docker Compose location.' + docker_compose_raw: + type: string + description: 'The Docker Compose raw content.' + docker_compose_custom_start_command: + type: string + description: 'The Docker Compose custom start command.' + docker_compose_custom_build_command: + type: string + description: 'The Docker Compose custom build command.' + docker_compose_domains: + type: array + description: 'The Docker Compose domains.' + watch_paths: + type: string + description: 'The watch paths.' + use_build_server: + type: boolean + nullable: true + description: 'Use build server.' + type: object + responses: + '200': + description: 'Application updated.' + content: + application/json: + schema: + properties: + uuid: { type: string } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/applications/{uuid}/envs': + get: + tags: + - Applications + summary: 'List Envs' + description: 'List all envs by application UUID.' + operationId: list-envs-by-application-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the application.' + required: true + schema: + type: string + format: uuid + responses: + '200': + description: 'All environment variables by application UUID.' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/EnvironmentVariable' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + post: + tags: + - Applications + summary: 'Create Env' + description: 'Create env by application UUID.' + operationId: create-env-by-application-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the application.' + required: true + schema: + type: string + format: uuid + requestBody: + description: 'Env created.' + required: true + content: + application/json: + schema: + properties: + key: + type: string + description: 'The key of the environment variable.' + value: + type: string + description: 'The value of the environment variable.' + is_preview: + type: boolean + description: 'The flag to indicate if the environment variable is used in preview deployments.' + is_build_time: + type: boolean + description: 'The flag to indicate if the environment variable is used in build time.' + is_literal: + type: boolean + description: 'The flag to indicate if the environment variable is a literal, nothing espaced.' + is_multiline: + type: boolean + description: 'The flag to indicate if the environment variable is multiline.' + is_shown_once: + type: boolean + description: "The flag to indicate if the environment variable's value is shown on the UI." + type: object + responses: + '201': + description: 'Environment variable created.' + content: + application/json: + schema: + properties: + uuid: { type: string, example: nc0k04gk8g0cgsk440g0koko } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + patch: + tags: + - Applications + summary: 'Update Env' + description: 'Update env by application UUID.' + operationId: update-env-by-application-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the application.' + required: true + schema: + type: string + format: uuid + requestBody: + description: 'Env updated.' + required: true + content: + application/json: + schema: + required: + - key + - value + properties: + key: + type: string + description: 'The key of the environment variable.' + value: + type: string + description: 'The value of the environment variable.' + is_preview: + type: boolean + description: 'The flag to indicate if the environment variable is used in preview deployments.' + is_build_time: + type: boolean + description: 'The flag to indicate if the environment variable is used in build time.' + is_literal: + type: boolean + description: 'The flag to indicate if the environment variable is a literal, nothing espaced.' + is_multiline: + type: boolean + description: 'The flag to indicate if the environment variable is multiline.' + is_shown_once: + type: boolean + description: "The flag to indicate if the environment variable's value is shown on the UI." + type: object + responses: + '201': + description: 'Environment variable updated.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Environment variable updated.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/applications/{uuid}/envs/bulk': + patch: + tags: + - Applications + summary: 'Update Envs (Bulk)' + description: 'Update multiple envs by application UUID.' + operationId: update-envs-by-application-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the application.' + required: true + schema: + type: string + format: uuid + requestBody: + description: 'Bulk envs updated.' + required: true + content: + application/json: + schema: + required: + - data + properties: + data: + type: array + items: { properties: { key: { type: string, description: 'The key of the environment variable.' }, value: { type: string, description: 'The value of the environment variable.' }, is_preview: { type: boolean, description: 'The flag to indicate if the environment variable is used in preview deployments.' }, is_build_time: { type: boolean, description: 'The flag to indicate if the environment variable is used in build time.' }, is_literal: { type: boolean, description: 'The flag to indicate if the environment variable is a literal, nothing espaced.' }, is_multiline: { type: boolean, description: 'The flag to indicate if the environment variable is multiline.' }, is_shown_once: { type: boolean, description: "The flag to indicate if the environment variable's value is shown on the UI." } }, type: object } + type: object + responses: + '201': + description: 'Environment variables updated.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Environment variables updated.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/applications/{uuid}/envs/{env_uuid}': + delete: + tags: + - Applications + summary: 'Delete Env' + description: 'Delete env by UUID.' + operationId: delete-env-by-application-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the application.' + required: true + schema: + type: string + format: uuid + - + name: env_uuid + in: path + description: 'UUID of the environment variable.' + required: true + schema: + type: string + format: uuid + responses: + '200': + description: 'Environment variable deleted.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Environment variable deleted.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/applications/{uuid}/start': + get: + tags: + - Applications + summary: Start + description: 'Start application. `Post` request is also accepted.' + operationId: start-application-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the application.' + required: true + schema: + type: string + format: uuid + - + name: force + in: query + description: 'Force rebuild.' + schema: + type: boolean + default: false + - + name: instant_deploy + in: query + description: 'Instant deploy (skip queuing).' + schema: + type: boolean + default: false + responses: + '200': + description: 'Start application.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Deployment request queued.', description: Message. } + deployment_uuid: { type: string, example: doogksw, description: 'UUID of the deployment.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/applications/{uuid}/stop': + get: + tags: + - Applications + summary: Stop + description: 'Stop application. `Post` request is also accepted.' + operationId: stop-application-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the application.' + required: true + schema: + type: string + format: uuid + responses: + '200': + description: 'Stop application.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Application stopping request queued.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/applications/{uuid}/restart': + get: + tags: + - Applications + summary: Restart + description: 'Restart application. `Post` request is also accepted.' + operationId: restart-application-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the application.' + required: true + schema: + type: string + format: uuid + responses: + '200': + description: 'Restart application.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Restart request queued.' } + deployment_uuid: { type: string, example: doogksw, description: 'UUID of the deployment.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/applications/{uuid}/execute': + post: + tags: + - Applications + summary: 'Execute Command' + description: "Execute a command on the application's current container." + operationId: execute-command-application + parameters: + - + name: uuid + in: path + description: 'UUID of the application.' + required: true + schema: + type: string + format: uuid + requestBody: + description: 'Command to execute.' + required: true + content: + application/json: + schema: + properties: + command: + type: string + description: 'Command to execute.' + type: object + responses: + '200': + description: "Execute a command on the application's current container." + content: + application/json: + schema: + properties: + message: { type: string, example: 'Command executed.' } + response: { type: string } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + /databases: + get: + tags: + - Databases + summary: List + description: 'List all databases.' + operationId: list-databases + responses: + '200': + description: 'Get all databases' + content: + application/json: + schema: + type: string + example: 'Content is very complex. Will be implemented later.' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + '/databases/{uuid}': + get: + tags: + - Databases + summary: Get + description: 'Get database by UUID.' + operationId: get-database-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the database.' + required: true + schema: + type: string + format: uuid + responses: + '200': + description: 'Get all databases' + content: + application/json: + schema: + type: string + example: 'Content is very complex. Will be implemented later.' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + delete: + tags: + - Databases + summary: Delete + description: 'Delete database by UUID.' + operationId: delete-database-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the database.' + required: true + schema: + type: string + format: uuid + - + name: delete_configurations + in: query + description: 'Delete configurations.' + required: false + schema: + type: boolean + default: true + - + name: delete_volumes + in: query + description: 'Delete volumes.' + required: false + schema: + type: boolean + default: true + - + name: docker_cleanup + in: query + description: 'Run docker cleanup.' + required: false + schema: + type: boolean + default: true + - + name: delete_connected_networks + in: query + description: 'Delete connected networks.' + required: false + schema: + type: boolean + default: true + responses: + '200': + description: 'Database deleted.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Database deleted.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + patch: + tags: + - Databases + summary: Update + description: 'Update database by UUID.' + operationId: update-database-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the database.' + required: true + schema: + type: string + format: uuid + requestBody: + description: 'Database data' + required: true + content: + application/json: + schema: + properties: + name: + type: string + description: 'Name of the database' + description: + type: string + description: 'Description of the database' + image: + type: string + description: 'Docker Image of the database' + is_public: + type: boolean + description: 'Is the database public?' + public_port: + type: integer + description: 'Public port of the database' + limits_memory: + type: string + description: 'Memory limit of the database' + limits_memory_swap: + type: string + description: 'Memory swap limit of the database' + limits_memory_swappiness: + type: integer + description: 'Memory swappiness of the database' + limits_memory_reservation: + type: string + description: 'Memory reservation of the database' + limits_cpus: + type: string + description: 'CPU limit of the database' + limits_cpuset: + type: string + description: 'CPU set of the database' + limits_cpu_shares: + type: integer + description: 'CPU shares of the database' + postgres_user: + type: string + description: 'PostgreSQL user' + postgres_password: + type: string + description: 'PostgreSQL password' + postgres_db: + type: string + description: 'PostgreSQL database' + postgres_initdb_args: + type: string + description: 'PostgreSQL initdb args' + postgres_host_auth_method: + type: string + description: 'PostgreSQL host auth method' + postgres_conf: + type: string + description: 'PostgreSQL conf' + clickhouse_admin_user: + type: string + description: 'Clickhouse admin user' + clickhouse_admin_password: + type: string + description: 'Clickhouse admin password' + dragonfly_password: + type: string + description: 'DragonFly password' + redis_password: + type: string + description: 'Redis password' + redis_conf: + type: string + description: 'Redis conf' + keydb_password: + type: string + description: 'KeyDB password' + keydb_conf: + type: string + description: 'KeyDB conf' + mariadb_conf: + type: string + description: 'MariaDB conf' + mariadb_root_password: + type: string + description: 'MariaDB root password' + mariadb_user: + type: string + description: 'MariaDB user' + mariadb_password: + type: string + description: 'MariaDB password' + mariadb_database: + type: string + description: 'MariaDB database' + mongo_conf: + type: string + description: 'Mongo conf' + mongo_initdb_root_username: + type: string + description: 'Mongo initdb root username' + mongo_initdb_root_password: + type: string + description: 'Mongo initdb root password' + mongo_initdb_init_database: + type: string + description: 'Mongo initdb init database' + mysql_root_password: + type: string + description: 'MySQL root password' + mysql_user: + type: string + description: 'MySQL user' + mysql_database: + type: string + description: 'MySQL database' + mysql_conf: + type: string + description: 'MySQL conf' + type: object + responses: + '200': + description: 'Database updated' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + /databases/postgresql: + post: + tags: + - Databases + summary: 'Create (PostgreSQL)' + description: 'Create a new PostgreSQL database.' + operationId: create-database-postgresql + requestBody: + description: 'Database data' + required: true + content: + application/json: + schema: + required: + - server_uuid + - project_uuid + - environment_name + properties: + server_uuid: + type: string + description: 'UUID of the server' + project_uuid: + type: string + description: 'UUID of the project' + environment_name: + type: string + description: 'Name of the environment' + postgres_user: + type: string + description: 'PostgreSQL user' + postgres_password: + type: string + description: 'PostgreSQL password' + postgres_db: + type: string + description: 'PostgreSQL database' + postgres_initdb_args: + type: string + description: 'PostgreSQL initdb args' + postgres_host_auth_method: + type: string + description: 'PostgreSQL host auth method' + postgres_conf: + type: string + description: 'PostgreSQL conf' + destination_uuid: + type: string + description: 'UUID of the destination if the server has multiple destinations' + name: + type: string + description: 'Name of the database' + description: + type: string + description: 'Description of the database' + image: + type: string + description: 'Docker Image of the database' + is_public: + type: boolean + description: 'Is the database public?' + public_port: + type: integer + description: 'Public port of the database' + limits_memory: + type: string + description: 'Memory limit of the database' + limits_memory_swap: + type: string + description: 'Memory swap limit of the database' + limits_memory_swappiness: + type: integer + description: 'Memory swappiness of the database' + limits_memory_reservation: + type: string + description: 'Memory reservation of the database' + limits_cpus: + type: string + description: 'CPU limit of the database' + limits_cpuset: + type: string + description: 'CPU set of the database' + limits_cpu_shares: + type: integer + description: 'CPU shares of the database' + instant_deploy: + type: boolean + description: 'Instant deploy the database' + type: object + responses: + '200': + description: 'Database updated' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + /databases/clickhouse: + post: + tags: + - Databases + summary: 'Create (Clickhouse)' + description: 'Create a new Clickhouse database.' + operationId: create-database-clickhouse + requestBody: + description: 'Database data' + required: true + content: + application/json: + schema: + required: + - server_uuid + - project_uuid + - environment_name + properties: + server_uuid: + type: string + description: 'UUID of the server' + project_uuid: + type: string + description: 'UUID of the project' + environment_name: + type: string + description: 'Name of the environment' + destination_uuid: + type: string + description: 'UUID of the destination if the server has multiple destinations' + clickhouse_admin_user: + type: string + description: 'Clickhouse admin user' + clickhouse_admin_password: + type: string + description: 'Clickhouse admin password' + name: + type: string + description: 'Name of the database' + description: + type: string + description: 'Description of the database' + image: + type: string + description: 'Docker Image of the database' + is_public: + type: boolean + description: 'Is the database public?' + public_port: + type: integer + description: 'Public port of the database' + limits_memory: + type: string + description: 'Memory limit of the database' + limits_memory_swap: + type: string + description: 'Memory swap limit of the database' + limits_memory_swappiness: + type: integer + description: 'Memory swappiness of the database' + limits_memory_reservation: + type: string + description: 'Memory reservation of the database' + limits_cpus: + type: string + description: 'CPU limit of the database' + limits_cpuset: + type: string + description: 'CPU set of the database' + limits_cpu_shares: + type: integer + description: 'CPU shares of the database' + instant_deploy: + type: boolean + description: 'Instant deploy the database' + type: object + responses: + '200': + description: 'Database updated' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + /databases/dragonfly: + post: + tags: + - Databases + summary: 'Create (DragonFly)' + description: 'Create a new DragonFly database.' + operationId: create-database-dragonfly + requestBody: + description: 'Database data' + required: true + content: + application/json: + schema: + required: + - server_uuid + - project_uuid + - environment_name + properties: + server_uuid: + type: string + description: 'UUID of the server' + project_uuid: + type: string + description: 'UUID of the project' + environment_name: + type: string + description: 'Name of the environment' + destination_uuid: + type: string + description: 'UUID of the destination if the server has multiple destinations' + dragonfly_password: + type: string + description: 'DragonFly password' + name: + type: string + description: 'Name of the database' + description: + type: string + description: 'Description of the database' + image: + type: string + description: 'Docker Image of the database' + is_public: + type: boolean + description: 'Is the database public?' + public_port: + type: integer + description: 'Public port of the database' + limits_memory: + type: string + description: 'Memory limit of the database' + limits_memory_swap: + type: string + description: 'Memory swap limit of the database' + limits_memory_swappiness: + type: integer + description: 'Memory swappiness of the database' + limits_memory_reservation: + type: string + description: 'Memory reservation of the database' + limits_cpus: + type: string + description: 'CPU limit of the database' + limits_cpuset: + type: string + description: 'CPU set of the database' + limits_cpu_shares: + type: integer + description: 'CPU shares of the database' + instant_deploy: + type: boolean + description: 'Instant deploy the database' + type: object + responses: + '200': + description: 'Database updated' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + /databases/redis: + post: + tags: + - Databases + summary: 'Create (Redis)' + description: 'Create a new Redis database.' + operationId: create-database-redis + requestBody: + description: 'Database data' + required: true + content: + application/json: + schema: + required: + - server_uuid + - project_uuid + - environment_name + properties: + server_uuid: + type: string + description: 'UUID of the server' + project_uuid: + type: string + description: 'UUID of the project' + environment_name: + type: string + description: 'Name of the environment' + destination_uuid: + type: string + description: 'UUID of the destination if the server has multiple destinations' + redis_password: + type: string + description: 'Redis password' + redis_conf: + type: string + description: 'Redis conf' + name: + type: string + description: 'Name of the database' + description: + type: string + description: 'Description of the database' + image: + type: string + description: 'Docker Image of the database' + is_public: + type: boolean + description: 'Is the database public?' + public_port: + type: integer + description: 'Public port of the database' + limits_memory: + type: string + description: 'Memory limit of the database' + limits_memory_swap: + type: string + description: 'Memory swap limit of the database' + limits_memory_swappiness: + type: integer + description: 'Memory swappiness of the database' + limits_memory_reservation: + type: string + description: 'Memory reservation of the database' + limits_cpus: + type: string + description: 'CPU limit of the database' + limits_cpuset: + type: string + description: 'CPU set of the database' + limits_cpu_shares: + type: integer + description: 'CPU shares of the database' + instant_deploy: + type: boolean + description: 'Instant deploy the database' + type: object + responses: + '200': + description: 'Database updated' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + /databases/keydb: + post: + tags: + - Databases + summary: 'Create (KeyDB)' + description: 'Create a new KeyDB database.' + operationId: create-database-keydb + requestBody: + description: 'Database data' + required: true + content: + application/json: + schema: + required: + - server_uuid + - project_uuid + - environment_name + properties: + server_uuid: + type: string + description: 'UUID of the server' + project_uuid: + type: string + description: 'UUID of the project' + environment_name: + type: string + description: 'Name of the environment' + destination_uuid: + type: string + description: 'UUID of the destination if the server has multiple destinations' + keydb_password: + type: string + description: 'KeyDB password' + keydb_conf: + type: string + description: 'KeyDB conf' + name: + type: string + description: 'Name of the database' + description: + type: string + description: 'Description of the database' + image: + type: string + description: 'Docker Image of the database' + is_public: + type: boolean + description: 'Is the database public?' + public_port: + type: integer + description: 'Public port of the database' + limits_memory: + type: string + description: 'Memory limit of the database' + limits_memory_swap: + type: string + description: 'Memory swap limit of the database' + limits_memory_swappiness: + type: integer + description: 'Memory swappiness of the database' + limits_memory_reservation: + type: string + description: 'Memory reservation of the database' + limits_cpus: + type: string + description: 'CPU limit of the database' + limits_cpuset: + type: string + description: 'CPU set of the database' + limits_cpu_shares: + type: integer + description: 'CPU shares of the database' + instant_deploy: + type: boolean + description: 'Instant deploy the database' + type: object + responses: + '200': + description: 'Database updated' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + /databases/mariadb: + post: + tags: + - Databases + summary: 'Create (MariaDB)' + description: 'Create a new MariaDB database.' + operationId: create-database-mariadb + requestBody: + description: 'Database data' + required: true + content: + application/json: + schema: + required: + - server_uuid + - project_uuid + - environment_name + properties: + server_uuid: + type: string + description: 'UUID of the server' + project_uuid: + type: string + description: 'UUID of the project' + environment_name: + type: string + description: 'Name of the environment' + destination_uuid: + type: string + description: 'UUID of the destination if the server has multiple destinations' + mariadb_conf: + type: string + description: 'MariaDB conf' + mariadb_root_password: + type: string + description: 'MariaDB root password' + mariadb_user: + type: string + description: 'MariaDB user' + mariadb_password: + type: string + description: 'MariaDB password' + mariadb_database: + type: string + description: 'MariaDB database' + name: + type: string + description: 'Name of the database' + description: + type: string + description: 'Description of the database' + image: + type: string + description: 'Docker Image of the database' + is_public: + type: boolean + description: 'Is the database public?' + public_port: + type: integer + description: 'Public port of the database' + limits_memory: + type: string + description: 'Memory limit of the database' + limits_memory_swap: + type: string + description: 'Memory swap limit of the database' + limits_memory_swappiness: + type: integer + description: 'Memory swappiness of the database' + limits_memory_reservation: + type: string + description: 'Memory reservation of the database' + limits_cpus: + type: string + description: 'CPU limit of the database' + limits_cpuset: + type: string + description: 'CPU set of the database' + limits_cpu_shares: + type: integer + description: 'CPU shares of the database' + instant_deploy: + type: boolean + description: 'Instant deploy the database' + type: object + responses: + '200': + description: 'Database updated' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + /databases/mysql: + post: + tags: + - Databases + summary: 'Create (MySQL)' + description: 'Create a new MySQL database.' + operationId: create-database-mysql + requestBody: + description: 'Database data' + required: true + content: + application/json: + schema: + required: + - server_uuid + - project_uuid + - environment_name + properties: + server_uuid: + type: string + description: 'UUID of the server' + project_uuid: + type: string + description: 'UUID of the project' + environment_name: + type: string + description: 'Name of the environment' + destination_uuid: + type: string + description: 'UUID of the destination if the server has multiple destinations' + mysql_root_password: + type: string + description: 'MySQL root password' + mysql_user: + type: string + description: 'MySQL user' + mysql_database: + type: string + description: 'MySQL database' + mysql_conf: + type: string + description: 'MySQL conf' + name: + type: string + description: 'Name of the database' + description: + type: string + description: 'Description of the database' + image: + type: string + description: 'Docker Image of the database' + is_public: + type: boolean + description: 'Is the database public?' + public_port: + type: integer + description: 'Public port of the database' + limits_memory: + type: string + description: 'Memory limit of the database' + limits_memory_swap: + type: string + description: 'Memory swap limit of the database' + limits_memory_swappiness: + type: integer + description: 'Memory swappiness of the database' + limits_memory_reservation: + type: string + description: 'Memory reservation of the database' + limits_cpus: + type: string + description: 'CPU limit of the database' + limits_cpuset: + type: string + description: 'CPU set of the database' + limits_cpu_shares: + type: integer + description: 'CPU shares of the database' + instant_deploy: + type: boolean + description: 'Instant deploy the database' + type: object + responses: + '200': + description: 'Database updated' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + /databases/mongodb: + post: + tags: + - Databases + summary: 'Create (MongoDB)' + description: 'Create a new MongoDB database.' + operationId: create-database-mongodb + requestBody: + description: 'Database data' + required: true + content: + application/json: + schema: + required: + - server_uuid + - project_uuid + - environment_name + properties: + server_uuid: + type: string + description: 'UUID of the server' + project_uuid: + type: string + description: 'UUID of the project' + environment_name: + type: string + description: 'Name of the environment' + destination_uuid: + type: string + description: 'UUID of the destination if the server has multiple destinations' + mongo_conf: + type: string + description: 'MongoDB conf' + mongo_initdb_root_username: + type: string + description: 'MongoDB initdb root username' + name: + type: string + description: 'Name of the database' + description: + type: string + description: 'Description of the database' + image: + type: string + description: 'Docker Image of the database' + is_public: + type: boolean + description: 'Is the database public?' + public_port: + type: integer + description: 'Public port of the database' + limits_memory: + type: string + description: 'Memory limit of the database' + limits_memory_swap: + type: string + description: 'Memory swap limit of the database' + limits_memory_swappiness: + type: integer + description: 'Memory swappiness of the database' + limits_memory_reservation: + type: string + description: 'Memory reservation of the database' + limits_cpus: + type: string + description: 'CPU limit of the database' + limits_cpuset: + type: string + description: 'CPU set of the database' + limits_cpu_shares: + type: integer + description: 'CPU shares of the database' + instant_deploy: + type: boolean + description: 'Instant deploy the database' + type: object + responses: + '200': + description: 'Database updated' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + '/databases/{uuid}/start': + get: + tags: + - Databases + summary: Start + description: 'Start database. `Post` request is also accepted.' + operationId: start-database-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the database.' + required: true + schema: + type: string + format: uuid + responses: + '200': + description: 'Start database.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Database starting request queued.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/databases/{uuid}/stop': + get: + tags: + - Databases + summary: Stop + description: 'Stop database. `Post` request is also accepted.' + operationId: stop-database-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the database.' + required: true + schema: + type: string + format: uuid + responses: + '200': + description: 'Stop database.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Database stopping request queued.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/databases/{uuid}/restart': + get: + tags: + - Databases + summary: Restart + description: 'Restart database. `Post` request is also accepted.' + operationId: restart-database-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the database.' + required: true + schema: + type: string + format: uuid + responses: + '200': + description: 'Restart database.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Database restaring request queued.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + /deployments: + get: + tags: + - Deployments + summary: List + description: 'List currently running deployments' + operationId: list-deployments + responses: + '200': + description: 'Get all currently running deployments.' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ApplicationDeploymentQueue' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + '/deployments/{uuid}': + get: + tags: + - Deployments + summary: Get + description: 'Get deployment by UUID.' + operationId: get-deployment-by-uuid + parameters: + - + name: uuid + in: path + description: 'Deployment UUID' + required: true + schema: + type: string + responses: + '200': + description: 'Get deployment by UUID.' + content: + application/json: + schema: + $ref: '#/components/schemas/ApplicationDeploymentQueue' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + /deploy: + get: + tags: + - Deployments + summary: Deploy + description: 'Deploy by tag or uuid. `Post` request also accepted.' + operationId: deploy-by-tag-or-uuid + parameters: + - + name: tag + in: query + description: 'Tag name(s). Comma separated list is also accepted.' + schema: + type: string + - + name: uuid + in: query + description: 'Resource UUID(s). Comma separated list is also accepted.' + schema: + type: string + - + name: force + in: query + description: 'Force rebuild (without cache)' + schema: + type: boolean + responses: + '200': + description: "Get deployment(s) UUID's" + content: + application/json: + schema: + properties: + deployments: { type: array, items: { properties: { message: { type: string }, resource_uuid: { type: string }, deployment_uuid: { type: string } }, type: object } } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + /version: + get: + summary: Version + description: 'Get Coolify version.' + operationId: version + responses: + '200': + description: 'Returns the version of the application' + content: + application/json: + schema: + type: string + example: v4.0.0 + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + /enable: + get: + summary: 'Enable API' + description: 'Enable API (only with root permissions).' + operationId: enable-api + responses: + '200': + description: 'Enable API.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'API enabled.' } + type: object + '403': + description: 'You are not allowed to enable the API.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'You are not allowed to enable the API.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + /disable: + get: + summary: 'Disable API' + description: 'Disable API (only with root permissions).' + operationId: disable-api + responses: + '200': + description: 'Disable API.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'API disabled.' } + type: object + '403': + description: 'You are not allowed to disable the API.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'You are not allowed to disable the API.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + /health: + get: + summary: Healthcheck + description: 'Healthcheck endpoint.' + operationId: healthcheck + responses: + '200': + description: 'Healthcheck endpoint.' + content: + application/json: + schema: + type: string + example: OK + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + /projects: + get: + tags: + - Projects + summary: List + description: 'List projects.' + operationId: list-projects + responses: + '200': + description: 'Get all projects.' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Project' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + post: + tags: + - Projects + summary: Create + description: 'Create Project.' + operationId: create-project + requestBody: + description: 'Project created.' + required: true + content: + application/json: + schema: + properties: + name: + type: string + description: 'The name of the project.' + description: + type: string + description: 'The description of the project.' + type: object + responses: + '201': + description: 'Project created.' + content: + application/json: + schema: + properties: + uuid: { type: string, example: og888os, description: 'The UUID of the project.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/projects/{uuid}': + get: + tags: + - Projects + summary: Get + description: 'Get project by UUID.' + operationId: get-project-by-uuid + parameters: + - + name: uuid + in: path + description: 'Project UUID' + required: true + schema: + type: string + responses: + '200': + description: 'Project details' + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + description: 'Project not found.' + security: + - + bearerAuth: [] + delete: + tags: + - Projects + summary: Delete + description: 'Delete project by UUID.' + operationId: delete-project-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the application.' + required: true + schema: + type: string + format: uuid + responses: + '200': + description: 'Project deleted.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Project deleted.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + patch: + tags: + - Projects + summary: Update + description: 'Update Project.' + operationId: update-project-by-uuid + requestBody: + description: 'Project updated.' + required: true + content: + application/json: + schema: + properties: + name: + type: string + description: 'The name of the project.' + description: + type: string + description: 'The description of the project.' + type: object + responses: + '201': + description: 'Project updated.' + content: + application/json: + schema: + properties: + uuid: { type: string, example: og888os } + name: { type: string, example: 'Project Name' } + description: { type: string, example: 'Project Description' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/projects/{uuid}/{environment_name}': + get: + tags: + - Projects + summary: Environment + description: 'Get environment by name.' + operationId: get-environment-by-name + parameters: + - + name: uuid + in: path + description: 'Project UUID' + required: true + schema: + type: string + - + name: environment_name + in: path + description: 'Environment name' + required: true + schema: + type: string + responses: + '200': + description: 'Environment details' + content: + application/json: + schema: + $ref: '#/components/schemas/Environment' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + /resources: + get: + tags: + - Resources + summary: List + description: 'Get all resources.' + operationId: list-resources + responses: + '200': + description: 'Get all resources' + content: + application/json: + schema: + type: string + example: 'Content is very complex. Will be implemented later.' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + /security/keys: + get: + tags: + - 'Private Keys' + summary: List + description: 'List all private keys.' + operationId: list-private-keys + responses: + '200': + description: 'Get all private keys.' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PrivateKey' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + post: + tags: + - 'Private Keys' + summary: Create + description: 'Create a new private key.' + operationId: create-private-key + requestBody: + required: true + content: + application/json: + schema: + required: + - private_key + properties: + name: + type: string + description: + type: string + private_key: + type: string + type: object + additionalProperties: false + responses: + '201': + description: "The created private key's UUID." + content: + application/json: + schema: + properties: + uuid: { type: string } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + patch: + tags: + - 'Private Keys' + summary: Update + description: 'Update a private key.' + operationId: update-private-key + requestBody: + required: true + content: + application/json: + schema: + required: + - private_key + properties: + name: + type: string + description: + type: string + private_key: + type: string + type: object + additionalProperties: false + responses: + '201': + description: "The updated private key's UUID." + content: + application/json: + schema: + properties: + uuid: { type: string } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + '/security/keys/{uuid}': + get: + tags: + - 'Private Keys' + summary: Get + description: 'Get key by UUID.' + operationId: get-private-key-by-uuid + parameters: + - + name: uuid + in: path + description: 'Private Key UUID' + required: true + schema: + type: string + responses: + '200': + description: 'Get all private keys.' + content: + application/json: + schema: + $ref: '#/components/schemas/PrivateKey' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + description: 'Private Key not found.' + security: + - + bearerAuth: [] + delete: + tags: + - 'Private Keys' + summary: Delete + description: 'Delete a private key.' + operationId: delete-private-key-by-uuid + parameters: + - + name: uuid + in: path + description: 'Private Key UUID' + required: true + schema: + type: string + responses: + '200': + description: 'Private Key deleted.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Private Key deleted.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + description: 'Private Key not found.' + security: + - + bearerAuth: [] + /servers: + get: + tags: + - Servers + summary: List + description: 'List all servers.' + operationId: list-servers + responses: + '200': + description: 'Get all servers.' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Server' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + post: + tags: + - Servers + summary: Create + description: 'Create Server.' + operationId: create-server + requestBody: + description: 'Server created.' + required: true + content: + application/json: + schema: + properties: + name: + type: string + example: 'My Server' + description: 'The name of the server.' + description: + type: string + example: 'My Server Description' + description: 'The description of the server.' + ip: + type: string + example: 127.0.0.1 + description: 'The IP of the server.' + port: + type: integer + example: 22 + description: 'The port of the server.' + user: + type: string + example: root + description: 'The user of the server.' + private_key_uuid: + type: string + example: og888os + description: 'The UUID of the private key.' + is_build_server: + type: boolean + example: false + description: 'Is build server.' + instant_validate: + type: boolean + example: false + description: 'Instant validate.' + type: object + responses: + '201': + description: 'Server created.' + content: + application/json: + schema: + properties: + uuid: { type: string, example: og888os, description: 'The UUID of the server.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/servers/{uuid}': + get: + tags: + - Servers + summary: Get + description: 'Get server by UUID.' + operationId: get-server-by-uuid + parameters: + - + name: uuid + in: path + description: "Server's UUID" + required: true + schema: + type: string + responses: + '200': + description: 'Get server by UUID' + content: + application/json: + schema: + $ref: '#/components/schemas/Server' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + delete: + tags: + - Servers + summary: Delete + description: 'Delete server by UUID.' + operationId: delete-server-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the server.' + required: true + schema: + type: string + format: uuid + responses: + '200': + description: 'Server deleted.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Server deleted.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + patch: + tags: + - Servers + summary: Update + description: 'Update Server.' + operationId: update-server-by-uuid + requestBody: + description: 'Server updated.' + required: true + content: + application/json: + schema: + properties: + name: + type: string + description: 'The name of the server.' + description: + type: string + description: 'The description of the server.' + ip: + type: string + description: 'The IP of the server.' + port: + type: integer + description: 'The port of the server.' + user: + type: string + description: 'The user of the server.' + private_key_uuid: + type: string + description: 'The UUID of the private key.' + is_build_server: + type: boolean + description: 'Is build server.' + instant_validate: + type: boolean + description: 'Instant validate.' + type: object + responses: + '201': + description: 'Server updated.' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Server' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/servers/{uuid}/resources': + get: + tags: + - Servers + summary: Resources + description: 'Get resources by server.' + operationId: get-resources-by-server-uuid + parameters: + - + name: uuid + in: path + description: "Server's UUID" + required: true + schema: + type: string + responses: + '200': + description: 'Get resources by server' + content: + application/json: + schema: + type: array + items: + properties: { id: { type: integer }, uuid: { type: string }, name: { type: string }, type: { type: string }, created_at: { type: string }, updated_at: { type: string }, status: { type: string } } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + '/servers/{uuid}/domains': + get: + tags: + - Servers + summary: Domains + description: 'Get domains by server.' + operationId: get-domains-by-server-uuid + parameters: + - + name: uuid + in: path + description: "Server's UUID" + required: true + schema: + type: string + responses: + '200': + description: 'Get domains by server' + content: + application/json: + schema: + type: array + items: + properties: { ip: { type: string }, domains: { type: array, items: { type: string } } } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + '/servers/{uuid}/validate': + get: + tags: + - Servers + summary: Validate + description: 'Validate server by UUID.' + operationId: validate-server-by-uuid + parameters: + - + name: uuid + in: path + description: 'Server UUID' + required: true + schema: + type: string + responses: + '201': + description: 'Server validation started.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Validation started.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + /services: + get: + tags: + - Services + summary: List + description: 'List all services.' + operationId: list-services + responses: + '200': + description: 'Get all services' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Service' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + post: + tags: + - Services + summary: Create + description: 'Create a one-click service' + operationId: create-service + requestBody: + required: true + content: + application/json: + schema: + required: + - server_uuid + - project_uuid + - environment_name + - type + properties: + type: + description: 'The one-click service type' + type: string + enum: [activepieces, appsmith, appwrite, authentik, babybuddy, budge, changedetection, chatwoot, classicpress-with-mariadb, classicpress-with-mysql, classicpress-without-database, cloudflared, code-server, dashboard, directus, directus-with-postgresql, docker-registry, docuseal, docuseal-with-postgres, dokuwiki, duplicati, emby, embystat, fider, filebrowser, firefly, formbricks, ghost, gitea, gitea-with-mariadb, gitea-with-mysql, gitea-with-postgresql, glance, glances, glitchtip, grafana, grafana-with-postgresql, grocy, heimdall, homepage, jellyfin, kuzzle, listmonk, logto, mediawiki, meilisearch, metabase, metube, minio, moodle, n8n, n8n-with-postgresql, next-image-transformation, nextcloud, nocodb, odoo, openblocks, pairdrop, penpot, phpmyadmin, pocketbase, posthog, reactive-resume, rocketchat, shlink, slash, snapdrop, statusnook, stirling-pdf, supabase, syncthing, tolgee, trigger, trigger-with-external-database, twenty, umami, unleash-with-postgresql, unleash-without-database, uptime-kuma, vaultwarden, vikunja, weblate, whoogle, wordpress-with-mariadb, wordpress-with-mysql, wordpress-without-database] + name: + type: string + maxLength: 255 + description: 'Name of the service.' + description: + type: string + nullable: true + description: 'Description of the service.' + project_uuid: + type: string + description: 'Project UUID.' + environment_name: + type: string + description: 'Environment name.' + server_uuid: + type: string + description: 'Server UUID.' + destination_uuid: + type: string + description: 'Destination UUID. Required if server has multiple destinations.' + instant_deploy: + type: boolean + default: false + description: 'Start the service immediately after creation.' + type: object + responses: + '201': + description: 'Create a service.' + content: + application/json: + schema: + properties: + uuid: { type: string, description: 'Service UUID.' } + domains: { type: array, items: { type: string }, description: 'Service domains.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + '/services/{uuid}': + get: + tags: + - Services + summary: Get + description: 'Get service by UUID.' + operationId: get-service-by-uuid + parameters: + - + name: uuid + in: path + description: 'Service UUID' + required: true + schema: + type: string + responses: + '200': + description: 'Get a service by UUID.' + content: + application/json: + schema: + $ref: '#/components/schemas/Service' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + delete: + tags: + - Services + summary: Delete + description: 'Delete service by UUID.' + operationId: delete-service-by-uuid + parameters: + - + name: uuid + in: path + description: 'Service UUID' + required: true + schema: + type: string + - + name: delete_configurations + in: query + description: 'Delete configurations.' + required: false + schema: + type: boolean + default: true + - + name: delete_volumes + in: query + description: 'Delete volumes.' + required: false + schema: + type: boolean + default: true + - + name: docker_cleanup + in: query + description: 'Run docker cleanup.' + required: false + schema: + type: boolean + default: true + - + name: delete_connected_networks + in: query + description: 'Delete connected networks.' + required: false + schema: + type: boolean + default: true + responses: + '200': + description: 'Delete a service by UUID' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Service deletion request queued.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/services/{uuid}/envs': + get: + tags: + - Services + summary: 'List Envs' + description: 'List all envs by service UUID.' + operationId: list-envs-by-service-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the service.' + required: true + schema: + type: string + format: uuid + responses: + '200': + description: 'All environment variables by service UUID.' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/EnvironmentVariable' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + post: + tags: + - Services + summary: 'Create Env' + description: 'Create env by service UUID.' + operationId: create-env-by-service-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the service.' + required: true + schema: + type: string + format: uuid + requestBody: + description: 'Env created.' + required: true + content: + application/json: + schema: + properties: + key: + type: string + description: 'The key of the environment variable.' + value: + type: string + description: 'The value of the environment variable.' + is_preview: + type: boolean + description: 'The flag to indicate if the environment variable is used in preview deployments.' + is_build_time: + type: boolean + description: 'The flag to indicate if the environment variable is used in build time.' + is_literal: + type: boolean + description: 'The flag to indicate if the environment variable is a literal, nothing espaced.' + is_multiline: + type: boolean + description: 'The flag to indicate if the environment variable is multiline.' + is_shown_once: + type: boolean + description: "The flag to indicate if the environment variable's value is shown on the UI." + type: object + responses: + '201': + description: 'Environment variable created.' + content: + application/json: + schema: + properties: + uuid: { type: string, example: nc0k04gk8g0cgsk440g0koko } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + patch: + tags: + - Services + summary: 'Update Env' + description: 'Update env by service UUID.' + operationId: update-env-by-service-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the service.' + required: true + schema: + type: string + format: uuid + requestBody: + description: 'Env updated.' + required: true + content: + application/json: + schema: + required: + - key + - value + properties: + key: + type: string + description: 'The key of the environment variable.' + value: + type: string + description: 'The value of the environment variable.' + is_preview: + type: boolean + description: 'The flag to indicate if the environment variable is used in preview deployments.' + is_build_time: + type: boolean + description: 'The flag to indicate if the environment variable is used in build time.' + is_literal: + type: boolean + description: 'The flag to indicate if the environment variable is a literal, nothing espaced.' + is_multiline: + type: boolean + description: 'The flag to indicate if the environment variable is multiline.' + is_shown_once: + type: boolean + description: "The flag to indicate if the environment variable's value is shown on the UI." + type: object + responses: + '201': + description: 'Environment variable updated.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Environment variable updated.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/services/{uuid}/envs/bulk': + patch: + tags: + - Services + summary: 'Update Envs (Bulk)' + description: 'Update multiple envs by service UUID.' + operationId: update-envs-by-service-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the service.' + required: true + schema: + type: string + format: uuid + requestBody: + description: 'Bulk envs updated.' + required: true + content: + application/json: + schema: + required: + - data + properties: + data: + type: array + items: { properties: { key: { type: string, description: 'The key of the environment variable.' }, value: { type: string, description: 'The value of the environment variable.' }, is_preview: { type: boolean, description: 'The flag to indicate if the environment variable is used in preview deployments.' }, is_build_time: { type: boolean, description: 'The flag to indicate if the environment variable is used in build time.' }, is_literal: { type: boolean, description: 'The flag to indicate if the environment variable is a literal, nothing espaced.' }, is_multiline: { type: boolean, description: 'The flag to indicate if the environment variable is multiline.' }, is_shown_once: { type: boolean, description: "The flag to indicate if the environment variable's value is shown on the UI." } }, type: object } + type: object + responses: + '201': + description: 'Environment variables updated.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Environment variables updated.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/services/{uuid}/envs/{env_uuid}': + delete: + tags: + - Services + summary: 'Delete Env' + description: 'Delete env by UUID.' + operationId: delete-env-by-service-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the service.' + required: true + schema: + type: string + format: uuid + - + name: env_uuid + in: path + description: 'UUID of the environment variable.' + required: true + schema: + type: string + format: uuid + responses: + '200': + description: 'Environment variable deleted.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Environment variable deleted.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/services/{uuid}/start': + get: + tags: + - Services + summary: Start + description: 'Start service. `Post` request is also accepted.' + operationId: start-service-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the service.' + required: true + schema: + type: string + format: uuid + responses: + '200': + description: 'Start service.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Service starting request queued.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/services/{uuid}/stop': + get: + tags: + - Services + summary: Stop + description: 'Stop service. `Post` request is also accepted.' + operationId: stop-service-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the service.' + required: true + schema: + type: string + format: uuid + responses: + '200': + description: 'Stop service.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Service stopping request queued.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/services/{uuid}/restart': + get: + tags: + - Services + summary: Restart + description: 'Restart service. `Post` request is also accepted.' + operationId: restart-service-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the service.' + required: true + schema: + type: string + format: uuid + responses: + '200': + description: 'Restart service.' + content: + application/json: + schema: + properties: + message: { type: string, example: 'Service restaring request queued.' } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + /teams: + get: + tags: + - Teams + summary: List + description: 'Get all teams.' + operationId: list-teams + responses: + '200': + description: 'List of teams.' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Team' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + '/teams/{id}': + get: + tags: + - Teams + summary: Get + description: 'Get team by TeamId.' + operationId: get-team-by-id + parameters: + - + name: id + in: path + description: 'Team ID' + required: true + schema: + type: integer + responses: + '200': + description: 'List of teams.' + content: + application/json: + schema: + $ref: '#/components/schemas/Team' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/teams/{id}/members': + get: + tags: + - Teams + summary: Members + description: 'Get members by TeamId.' + operationId: get-members-by-team-id + parameters: + - + name: id + in: path + description: 'Team ID' + required: true + schema: + type: integer + responses: + '200': + description: 'List of members.' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + /teams/current: + get: + tags: + - Teams + summary: 'Authenticated Team' + description: 'Get currently authenticated team.' + operationId: get-current-team + responses: + '200': + description: 'Current Team.' + content: + application/json: + schema: + $ref: '#/components/schemas/Team' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] + /teams/current/members: + get: + tags: + - Teams + summary: 'Authenticated Team Members' + description: 'Get currently authenticated team members.' + operationId: get-current-team-members + responses: + '200': + description: 'Currently authenticated team members.' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + security: + - + bearerAuth: [] +components: + schemas: + Application: + description: 'Application model' + properties: + id: + type: integer + description: 'The application identifier in the database.' + description: + type: string + nullable: true + description: 'The application description.' + repository_project_id: + type: integer + nullable: true + description: 'The repository project identifier.' + uuid: + type: string + description: 'The application UUID.' + name: + type: string + description: 'The application name.' + fqdn: + type: string + nullable: true + description: 'The application domains.' + config_hash: + type: string + description: 'Configuration hash.' + git_repository: + type: string + description: 'Git repository URL.' + git_branch: + type: string + description: 'Git branch.' + git_commit_sha: + type: string + description: 'Git commit SHA.' + git_full_url: + type: string + nullable: true + description: 'Git full URL.' + docker_registry_image_name: + type: string + nullable: true + description: 'Docker registry image name.' + docker_registry_image_tag: + type: string + nullable: true + description: 'Docker registry image tag.' + build_pack: + type: string + description: 'Build pack.' + enum: + - nixpacks + - static + - dockerfile + - dockercompose + static_image: + type: string + description: 'Static image used when static site is deployed.' + install_command: + type: string + description: 'Install command.' + build_command: + type: string + description: 'Build command.' + start_command: + type: string + description: 'Start command.' + ports_exposes: + type: string + description: 'Ports exposes.' + ports_mappings: + type: string + nullable: true + description: 'Ports mappings.' + base_directory: + type: string + description: 'Base directory for all commands.' + publish_directory: + type: string + description: 'Publish directory.' + health_check_enabled: + type: boolean + description: 'Health check enabled.' + health_check_path: + type: string + description: 'Health check path.' + health_check_port: + type: string + nullable: true + description: 'Health check port.' + health_check_host: + type: string + nullable: true + description: 'Health check host.' + health_check_method: + type: string + description: 'Health check method.' + health_check_return_code: + type: integer + description: 'Health check return code.' + health_check_scheme: + type: string + description: 'Health check scheme.' + health_check_response_text: + type: string + nullable: true + description: 'Health check response text.' + health_check_interval: + type: integer + description: 'Health check interval in seconds.' + health_check_timeout: + type: integer + description: 'Health check timeout in seconds.' + health_check_retries: + type: integer + description: 'Health check retries count.' + health_check_start_period: + type: integer + description: 'Health check start period in seconds.' + limits_memory: + type: string + description: 'Memory limit.' + limits_memory_swap: + type: string + description: 'Memory swap limit.' + limits_memory_swappiness: + type: integer + description: 'Memory swappiness.' + limits_memory_reservation: + type: string + description: 'Memory reservation.' + limits_cpus: + type: string + description: 'CPU limit.' + limits_cpuset: + type: string + nullable: true + description: 'CPU set.' + limits_cpu_shares: + type: integer + description: 'CPU shares.' + status: + type: string + description: 'Application status.' + preview_url_template: + type: string + description: 'Preview URL template.' + destination_type: + type: string + description: 'Destination type.' + destination_id: + type: integer + description: 'Destination identifier.' + source_id: + type: integer + nullable: true + description: 'Source identifier.' + private_key_id: + type: integer + nullable: true + description: 'Private key identifier.' + environment_id: + type: integer + description: 'Environment identifier.' + dockerfile: + type: string + nullable: true + description: 'Dockerfile content. Used for dockerfile build pack.' + dockerfile_location: + type: string + description: 'Dockerfile location.' + custom_labels: + type: string + nullable: true + description: 'Custom labels.' + dockerfile_target_build: + type: string + nullable: true + description: 'Dockerfile target build.' + manual_webhook_secret_github: + type: string + nullable: true + description: 'Manual webhook secret for GitHub.' + manual_webhook_secret_gitlab: + type: string + nullable: true + description: 'Manual webhook secret for GitLab.' + manual_webhook_secret_bitbucket: + type: string + nullable: true + description: 'Manual webhook secret for Bitbucket.' + manual_webhook_secret_gitea: + type: string + nullable: true + description: 'Manual webhook secret for Gitea.' + docker_compose_location: + type: string + description: 'Docker compose location.' + docker_compose: + type: string + nullable: true + description: 'Docker compose content. Used for docker compose build pack.' + docker_compose_raw: + type: string + nullable: true + description: 'Docker compose raw content.' + docker_compose_domains: + type: string + nullable: true + description: 'Docker compose domains.' + docker_compose_custom_start_command: + type: string + nullable: true + description: 'Docker compose custom start command.' + docker_compose_custom_build_command: + type: string + nullable: true + description: 'Docker compose custom build command.' + swarm_replicas: + type: integer + nullable: true + description: 'Swarm replicas. Only used for swarm deployments.' + swarm_placement_constraints: + type: string + nullable: true + description: 'Swarm placement constraints. Only used for swarm deployments.' + custom_docker_run_options: + type: string + nullable: true + description: 'Custom docker run options.' + post_deployment_command: + type: string + nullable: true + description: 'Post deployment command.' + post_deployment_command_container: + type: string + nullable: true + description: 'Post deployment command container.' + pre_deployment_command: + type: string + nullable: true + description: 'Pre deployment command.' + pre_deployment_command_container: + type: string + nullable: true + description: 'Pre deployment command container.' + watch_paths: + type: string + nullable: true + description: 'Watch paths.' + custom_healthcheck_found: + type: boolean + description: 'Custom healthcheck found.' + redirect: + type: string + nullable: true + description: 'How to set redirect with Traefik / Caddy. www<->non-www.' + enum: + - www + - non-www + - both + created_at: + type: string + format: date-time + description: 'The date and time when the application was created.' + updated_at: + type: string + format: date-time + description: 'The date and time when the application was last updated.' + deleted_at: + type: string + format: date-time + nullable: true + description: 'The date and time when the application was deleted.' + compose_parsing_version: + type: string + description: 'How Coolify parse the compose file.' + custom_nginx_configuration: + type: string + nullable: true + description: 'Custom Nginx configuration base64 encoded.' + type: object + ApplicationDeploymentQueue: + description: 'Project model' + properties: + id: + type: integer + application_id: + type: string + deployment_uuid: + type: string + pull_request_id: + type: integer + force_rebuild: + type: boolean + commit: + type: string + status: + type: string + is_webhook: + type: boolean + is_api: + type: boolean + created_at: + type: string + updated_at: + type: string + logs: + type: string + current_process_id: + type: string + restart_only: + type: boolean + git_type: + type: string + server_id: + type: integer + application_name: + type: string + server_name: + type: string + deployment_url: + type: string + destination_id: + type: string + only_this_server: + type: boolean + rollback: + type: boolean + commit_message: + type: string + type: object + Environment: + description: 'Environment model' + properties: + id: + type: integer + name: + type: string + project_id: + type: integer + created_at: + type: string + updated_at: + type: string + description: + type: string + type: object + EnvironmentVariable: + description: 'Environment Variable model' + properties: + id: + type: integer + uuid: + type: string + application_id: + type: integer + service_id: + type: integer + database_id: + type: integer + is_build_time: + type: boolean + is_literal: + type: boolean + is_multiline: + type: boolean + is_preview: + type: boolean + is_shared: + type: boolean + is_shown_once: + type: boolean + key: + type: string + value: + type: string + real_value: + type: string + version: + type: string + created_at: + type: string + updated_at: + type: string + type: object + PrivateKey: + description: 'Private Key model' + properties: + id: + type: integer + uuid: + type: string + name: + type: string + description: + type: string + private_key: + type: string + format: private-key + is_git_related: + type: boolean + team_id: + type: integer + created_at: + type: string + updated_at: + type: string + type: object + Project: + description: 'Project model' + properties: + id: + type: integer + uuid: + type: string + name: + type: string + description: + type: string + environments: + description: 'The environments of the project.' + type: array + items: + $ref: '#/components/schemas/Environment' + type: object + Server: + description: 'Server model' + properties: + id: + type: integer + uuid: + type: string + name: + type: string + description: + type: string + ip: + type: string + user: + type: string + port: + type: integer + proxy: + type: object + high_disk_usage_notification_sent: + type: boolean + unreachable_notification_sent: + type: boolean + unreachable_count: + type: integer + validation_logs: + type: string + log_drain_notification_sent: + type: boolean + swarm_cluster: + type: string + delete_unused_volumes: + type: boolean + delete_unused_networks: + type: boolean + type: object + ServerSetting: + description: 'Server Settings model' + properties: + id: + type: integer + concurrent_builds: + type: integer + dynamic_timeout: + type: integer + force_disabled: + type: boolean + force_server_cleanup: + type: boolean + is_build_server: + type: boolean + is_cloudflare_tunnel: + type: boolean + is_jump_server: + type: boolean + is_logdrain_axiom_enabled: + type: boolean + is_logdrain_custom_enabled: + type: boolean + is_logdrain_highlight_enabled: + type: boolean + is_logdrain_newrelic_enabled: + type: boolean + is_metrics_enabled: + type: boolean + is_reachable: + type: boolean + is_sentinel_enabled: + type: boolean + is_swarm_manager: + type: boolean + is_swarm_worker: + type: boolean + is_usable: + type: boolean + logdrain_axiom_api_key: + type: string + logdrain_axiom_dataset_name: + type: string + logdrain_custom_config: + type: string + logdrain_custom_config_parser: + type: string + logdrain_highlight_project_id: + type: string + logdrain_newrelic_base_uri: + type: string + logdrain_newrelic_license_key: + type: string + sentinel_metrics_history_days: + type: integer + sentinel_metrics_refresh_rate_seconds: + type: integer + sentinel_token: + type: string + docker_cleanup_frequency: + type: string + docker_cleanup_threshold: + type: integer + server_id: + type: integer + wildcard_domain: + type: string + created_at: + type: string + updated_at: + type: string + type: object + Service: + description: 'Service model' + properties: + id: + type: integer + description: 'The unique identifier of the service. Only used for database identification.' + uuid: + type: string + description: 'The unique identifier of the service.' + name: + type: string + description: 'The name of the service.' + environment_id: + type: integer + description: 'The unique identifier of the environment where the service is attached to.' + server_id: + type: integer + description: 'The unique identifier of the server where the service is running.' + description: + type: string + description: 'The description of the service.' + docker_compose_raw: + type: string + description: 'The raw docker-compose.yml file of the service.' + docker_compose: + type: string + description: 'The docker-compose.yml file that is parsed and modified by Coolify.' + destination_type: + type: string + description: 'Destination type.' + destination_id: + type: integer + description: 'The unique identifier of the destination where the service is running.' + connect_to_docker_network: + type: boolean + description: 'The flag to connect the service to the predefined Docker network.' + is_container_label_escape_enabled: + type: boolean + description: 'The flag to enable the container label escape.' + is_container_label_readonly_enabled: + type: boolean + description: 'The flag to enable the container label readonly.' + config_hash: + type: string + description: 'The hash of the service configuration.' + service_type: + type: string + description: 'The type of the service.' + created_at: + type: string + description: 'The date and time when the service was created.' + updated_at: + type: string + description: 'The date and time when the service was last updated.' + deleted_at: + type: string + description: 'The date and time when the service was deleted.' + type: object + Team: + description: 'Team model' + properties: + id: + type: integer + description: 'The unique identifier of the team.' + name: + type: string + description: 'The name of the team.' + description: + type: string + description: 'The description of the team.' + personal_team: + type: boolean + description: 'Whether the team is personal or not.' + created_at: + type: string + description: 'The date and time the team was created.' + updated_at: + type: string + description: 'The date and time the team was last updated.' + smtp_enabled: + type: boolean + description: 'Whether SMTP is enabled or not.' + smtp_from_address: + type: string + description: 'The email address to send emails from.' + smtp_from_name: + type: string + description: 'The name to send emails from.' + smtp_recipients: + type: string + description: 'The email addresses to send emails to.' + smtp_host: + type: string + description: 'The SMTP host.' + smtp_port: + type: string + description: 'The SMTP port.' + smtp_encryption: + type: string + description: 'The SMTP encryption.' + smtp_username: + type: string + description: 'The SMTP username.' + smtp_password: + type: string + description: 'The SMTP password.' + smtp_timeout: + type: string + description: 'The SMTP timeout.' + smtp_notifications_test: + type: boolean + description: 'Whether to send test notifications via SMTP.' + smtp_notifications_deployments: + type: boolean + description: 'Whether to send deployment notifications via SMTP.' + smtp_notifications_status_changes: + type: boolean + description: 'Whether to send status change notifications via SMTP.' + smtp_notifications_scheduled_tasks: + type: boolean + description: 'Whether to send scheduled task notifications via SMTP.' + smtp_notifications_database_backups: + type: boolean + description: 'Whether to send database backup notifications via SMTP.' + smtp_notifications_server_disk_usage: + type: boolean + description: 'Whether to send server disk usage notifications via SMTP.' + discord_enabled: + type: boolean + description: 'Whether Discord is enabled or not.' + discord_webhook_url: + type: string + description: 'The Discord webhook URL.' + discord_notifications_test: + type: boolean + description: 'Whether to send test notifications via Discord.' + discord_notifications_deployments: + type: boolean + description: 'Whether to send deployment notifications via Discord.' + discord_notifications_status_changes: + type: boolean + description: 'Whether to send status change notifications via Discord.' + discord_notifications_database_backups: + type: boolean + description: 'Whether to send database backup notifications via Discord.' + discord_notifications_scheduled_tasks: + type: boolean + description: 'Whether to send scheduled task notifications via Discord.' + discord_notifications_server_disk_usage: + type: boolean + description: 'Whether to send server disk usage notifications via Discord.' + show_boarding: + type: boolean + description: 'Whether to show the boarding screen or not.' + resend_enabled: + type: boolean + description: 'Whether to enable resending or not.' + resend_api_key: + type: string + description: 'The resending API key.' + use_instance_email_settings: + type: boolean + description: 'Whether to use instance email settings or not.' + telegram_enabled: + type: boolean + description: 'Whether Telegram is enabled or not.' + telegram_token: + type: string + description: 'The Telegram token.' + telegram_chat_id: + type: string + description: 'The Telegram chat ID.' + telegram_notifications_test: + type: boolean + description: 'Whether to send test notifications via Telegram.' + telegram_notifications_deployments: + type: boolean + description: 'Whether to send deployment notifications via Telegram.' + telegram_notifications_status_changes: + type: boolean + description: 'Whether to send status change notifications via Telegram.' + telegram_notifications_database_backups: + type: boolean + description: 'Whether to send database backup notifications via Telegram.' + telegram_notifications_test_message_thread_id: + type: string + description: 'The Telegram test message thread ID.' + telegram_notifications_deployments_message_thread_id: + type: string + description: 'The Telegram deployment message thread ID.' + telegram_notifications_status_changes_message_thread_id: + type: string + description: 'The Telegram status change message thread ID.' + telegram_notifications_database_backups_message_thread_id: + type: string + description: 'The Telegram database backup message thread ID.' + custom_server_limit: + type: string + description: 'The custom server limit.' + telegram_notifications_scheduled_tasks: + type: boolean + description: 'Whether to send scheduled task notifications via Telegram.' + telegram_notifications_scheduled_tasks_thread_id: + type: string + description: 'The Telegram scheduled task message thread ID.' + members: + description: 'The members of the team.' + type: array + items: + $ref: '#/components/schemas/User' + type: object + User: + description: 'User model' + properties: + id: + type: integer + description: 'The user identifier in the database.' + name: + type: string + description: 'The user name.' + email: + type: string + description: 'The user email.' + email_verified_at: + type: string + description: 'The date when the user email was verified.' + created_at: + type: string + description: 'The date when the user was created.' + updated_at: + type: string + description: 'The date when the user was updated.' + two_factor_confirmed_at: + type: string + description: 'The date when the user two factor was confirmed.' + force_password_reset: + type: boolean + description: 'The flag to force the user to reset the password.' + marketing_emails: + type: boolean + description: 'The flag to receive marketing emails.' + type: object + responses: + '400': + description: 'Invalid token.' + content: + application/json: + schema: + properties: + message: + type: string + example: 'Invalid token.' + type: object + '401': + description: Unauthenticated. + content: + application/json: + schema: + properties: + message: + type: string + example: Unauthenticated. + type: object + '404': + description: 'Resource not found.' + content: + application/json: + schema: + properties: + message: + type: string + example: 'Resource not found.' + type: object + securitySchemes: + bearerAuth: + type: http + description: 'Go to `Keys & Tokens` / `API tokens` and create a new token. Use the token as the bearer token.' + scheme: bearer +tags: + - + name: Applications + description: Applications + - + name: Databases + description: Databases + - + name: Deployments + description: Deployments + - + name: Projects + description: Projects + - + name: Resources + description: Resources + - + name: 'Private Keys' + description: 'Private Keys' + - + name: Servers + description: Servers + - + name: Services + description: Services + - + name: Teams + description: Teams diff --git a/other/logos/advin.png b/other/logos/advin.png new file mode 100644 index 000000000..155408b9c Binary files /dev/null and b/other/logos/advin.png differ diff --git a/other/logos/blacksmith.svg b/other/logos/blacksmith.svg new file mode 100644 index 000000000..d8fbf0441 --- /dev/null +++ b/other/logos/blacksmith.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/other/logos/branddev.png b/other/logos/branddev.png new file mode 100644 index 000000000..ef259ff2e Binary files /dev/null and b/other/logos/branddev.png differ diff --git a/other/logos/codext.jpg b/other/logos/codext.jpg new file mode 100644 index 000000000..8abf63972 Binary files /dev/null and b/other/logos/codext.jpg differ diff --git a/other/logos/fractal.png b/other/logos/fractal.png new file mode 100644 index 000000000..c4d39c1f1 Binary files /dev/null and b/other/logos/fractal.png differ diff --git a/other/logos/fractal.svg b/other/logos/fractal.svg new file mode 100644 index 000000000..cd2ee4134 --- /dev/null +++ b/other/logos/fractal.svg @@ -0,0 +1,40 @@ + + + + + + Networks + Fractal + + + + + + + \ No newline at end of file diff --git a/other/logos/glueops.webp b/other/logos/glueops.webp new file mode 100644 index 000000000..d5acda999 Binary files /dev/null and b/other/logos/glueops.webp differ diff --git a/other/logos/hostinger.svg b/other/logos/hostinger.svg new file mode 100644 index 000000000..42ec702ab --- /dev/null +++ b/other/logos/hostinger.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/other/logos/jobscollider.svg b/other/logos/jobscollider.svg new file mode 100644 index 000000000..a57492f41 --- /dev/null +++ b/other/logos/jobscollider.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/other/logos/juxtdigital.png b/other/logos/juxtdigital.png new file mode 100644 index 000000000..92257bd35 Binary files /dev/null and b/other/logos/juxtdigital.png differ diff --git a/other/logos/latitude.svg b/other/logos/latitude.svg new file mode 100644 index 000000000..489d9ebc7 --- /dev/null +++ b/other/logos/latitude.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/other/logos/massivegrid.svg b/other/logos/massivegrid.svg new file mode 100644 index 000000000..09f7ba00f --- /dev/null +++ b/other/logos/massivegrid.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/other/logos/saasykit.png b/other/logos/saasykit.png new file mode 100644 index 000000000..27013eadd Binary files /dev/null and b/other/logos/saasykit.png differ diff --git a/other/logos/saasykit.webp b/other/logos/saasykit.webp new file mode 100644 index 000000000..0d1085e6c Binary files /dev/null and b/other/logos/saasykit.webp differ diff --git a/other/logos/supaguide.png b/other/logos/supaguide.png new file mode 100644 index 000000000..195f3ce92 Binary files /dev/null and b/other/logos/supaguide.png differ diff --git a/other/logos/tigris.svg b/other/logos/tigris.svg new file mode 100644 index 000000000..367c59f2d --- /dev/null +++ b/other/logos/tigris.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/other/logos/trieve_bg.png b/other/logos/trieve_bg.png new file mode 100644 index 000000000..5425fbd73 Binary files /dev/null and b/other/logos/trieve_bg.png differ diff --git a/other/logos/ubicloud.svg b/other/logos/ubicloud.svg new file mode 100644 index 000000000..3613858ca --- /dev/null +++ b/other/logos/ubicloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/other/nightly/.env.production b/other/nightly/.env.production new file mode 100644 index 000000000..099ec7c25 --- /dev/null +++ b/other/nightly/.env.production @@ -0,0 +1,16 @@ +# Coolify Configuration +APP_ID= +APP_NAME=Coolify +APP_KEY= + +# PostgreSQL Database Configuration +DB_USERNAME=coolify +DB_PASSWORD= + +# Redis Configuration +REDIS_PASSWORD= + +# Pusher Configuration +PUSHER_APP_ID= +PUSHER_APP_KEY= +PUSHER_APP_SECRET= diff --git a/other/nightly/docker-compose.prod.yml b/other/nightly/docker-compose.prod.yml new file mode 100644 index 000000000..d86b2336b --- /dev/null +++ b/other/nightly/docker-compose.prod.yml @@ -0,0 +1,122 @@ +services: + coolify: + image: "ghcr.io/coollabsio/coolify:${LATEST_IMAGE:-latest}" + volumes: + - type: bind + source: /data/coolify/source/.env + target: /var/www/html/.env + read_only: true + - /data/coolify/ssh:/var/www/html/storage/app/ssh + - /data/coolify/applications:/var/www/html/storage/app/applications + - /data/coolify/databases:/var/www/html/storage/app/databases + - /data/coolify/services:/var/www/html/storage/app/services + - /data/coolify/backups:/var/www/html/storage/app/backups + - /data/coolify/webhooks-during-maintenance:/var/www/html/storage/app/webhooks-during-maintenance + environment: + - APP_ENV=production + - APP_NAME + - APP_ID + - APP_KEY + - APP_URL + - APP_DEBUG + - DB_DATABASE + - DB_USERNAME + - DB_PASSWORD + - DB_HOST + - DB_PORT + - DB_CONNECTION + - QUEUE_CONNECTION + - REDIS_HOST + - REDIS_PASSWORD + - HORIZON_BALANCE + - HORIZON_MIN_PROCESSES + - HORIZON_MAX_PROCESSES + - HORIZON_BALANCE_MAX_SHIFT + - HORIZON_BALANCE_COOLDOWN + - SSL_MODE=off + - PHP_MEMORY_LIMIT + - PHP_PM_CONTROL=dynamic + - PHP_PM_START_SERVERS=1 + - PHP_PM_MIN_SPARE_SERVERS=1 + - PHP_PM_MAX_SPARE_SERVERS=10 + - PUSHER_HOST + - PUSHER_BACKEND_HOST + - PUSHER_PORT + - PUSHER_BACKEND_PORT + - PUSHER_SCHEME + - PUSHER_APP_ID + - PUSHER_APP_KEY + - PUSHER_APP_SECRET + - TERMINAL_PROTOCOL + - TERMINAL_HOST + - TERMINAL_PORT + - AUTOUPDATE + - SSH_MUX_ENABLED + - SSH_MUX_PERSIST_TIME + ports: + - "${APP_PORT:-8000}:80" + expose: + - "${APP_PORT:-8000}" + healthcheck: + test: curl --fail http://127.0.0.1:80/api/health || exit 1 + interval: 5s + retries: 10 + timeout: 2s + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + soketi: + condition: service_healthy + postgres: + volumes: + - coolify-db:/var/lib/postgresql/data + environment: + POSTGRES_USER: "${DB_USERNAME}" + POSTGRES_PASSWORD: "${DB_PASSWORD}" + POSTGRES_DB: "${DB_DATABASE:-coolify}" + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U ${DB_USERNAME}", "-d", "${DB_DATABASE:-coolify}" ] + interval: 5s + retries: 10 + timeout: 2s + redis: + command: redis-server --save 20 1 --loglevel warning --requirepass ${REDIS_PASSWORD} + environment: + REDIS_PASSWORD: "${REDIS_PASSWORD}" + volumes: + - coolify-redis:/data + healthcheck: + test: redis-cli ping + interval: 5s + retries: 10 + timeout: 2s + soketi: + image: 'ghcr.io/coollabsio/coolify-realtime:1.0.4' + ports: + - "${SOKETI_PORT:-6001}:6001" + - "6002:6002" + volumes: + - /data/coolify/ssh:/var/www/html/storage/app/ssh + environment: + APP_NAME: "${APP_NAME:-Coolify}" + SOKETI_DEBUG: "${SOKETI_DEBUG:-false}" + SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}" + SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}" + SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}" + healthcheck: + test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1" ] + interval: 5s + retries: 10 + timeout: 2s + +volumes: + coolify-db: + name: coolify-db + coolify-redis: + name: coolify-redis + +networks: + coolify: + external: true diff --git a/other/nightly/docker-compose.windows.yml b/other/nightly/docker-compose.windows.yml new file mode 100644 index 000000000..ef2de82e9 --- /dev/null +++ b/other/nightly/docker-compose.windows.yml @@ -0,0 +1,133 @@ +services: + coolify-testing-host: + init: true + image: "ghcr.io/coollabsio/coolify-testing-host:latest" + pull_policy: always + container_name: coolify-testing-host + volumes: + - //var/run/docker.sock://var/run/docker.sock + - ./:/data/coolify + coolify: + image: "ghcr.io/coollabsio/coolify:latest" + pull_policy: always + container_name: coolify + restart: always + working_dir: /var/www/html + extra_hosts: + - 'host.docker.internal:host-gateway' + volumes: + - type: bind + source: .env + target: /var/www/html/.env + read_only: true + - ./ssh:/var/www/html/storage/app/ssh + - ./applications:/var/www/html/storage/app/applications + - ./databases:/var/www/html/storage/app/databases + - ./services:/var/www/html/storage/app/services + - ./backups:/var/www/html/storage/app/backups + - ./webhooks-during-maintenance:/var/www/html/storage/app/webhooks-during-maintenance + env_file: + - .env + environment: + - APP_ID + - APP_ENV=production + - APP_NAME + - APP_KEY + - DB_PASSWORD + - REDIS_PASSWORD + - SSL_MODE=off + - PHP_PM_CONTROL=dynamic + - PHP_PM_START_SERVERS=1 + - PHP_PM_MIN_SPARE_SERVERS=1 + - PHP_PM_MAX_SPARE_SERVERS=10 + - PUSHER_APP_ID + - PUSHER_APP_KEY + - PUSHER_APP_SECRET + - AUTOUPDATE=true + - SELF_HOSTED=true + - SSH_MUX_ENABLED=false + - IS_WINDOWS_DOCKER_DESKTOP=true + ports: + - "${APP_PORT:-8000}:80" + expose: + - "${APP_PORT:-8000}" + healthcheck: + test: curl --fail http://localhost:80/api/health || exit 1 + interval: 5s + retries: 10 + timeout: 2s + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + postgres: + image: postgres:15-alpine + pull_policy: always + container_name: coolify-db + restart: always + env_file: + - .env + volumes: + - coolify-db:/var/lib/postgresql/data + environment: + POSTGRES_USER: "${DB_USERNAME}" + POSTGRES_PASSWORD: "${DB_PASSWORD}" + POSTGRES_DB: "${DB_DATABASE:-coolify}" + healthcheck: + test: + [ + "CMD-SHELL", + "pg_isready -U ${DB_USERNAME}", + "-d", + "${DB_DATABASE:-coolify}" + ] + interval: 5s + retries: 10 + timeout: 2s + redis: + image: redis:alpine + pull_policy: always + container_name: coolify-redis + restart: always + command: redis-server --save 20 1 --loglevel warning --requirepass ${REDIS_PASSWORD} + env_file: + - .env + environment: + REDIS_PASSWORD: "${REDIS_PASSWORD}" + volumes: + - coolify-redis:/data + healthcheck: + test: redis-cli ping + interval: 5s + retries: 10 + timeout: 2s + soketi: + image: 'ghcr.io/coollabsio/coolify-realtime:1.0.0' + pull_policy: always + container_name: coolify-realtime + restart: always + env_file: + - .env + ports: + - "${SOKETI_PORT:-6001}:6001" + - "6002:6002" + volumes: + - ./ssh:/var/www/html/storage/app/ssh + environment: + APP_NAME: "${APP_NAME:-Coolify}" + SOKETI_DEBUG: "${SOKETI_DEBUG:-false}" + SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}" + SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}" + SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1"] + interval: 5s + retries: 10 + timeout: 2s + +volumes: + coolify-db: + name: coolify-db + coolify-redis: + name: coolify-redis diff --git a/other/nightly/docker-compose.yml b/other/nightly/docker-compose.yml new file mode 100644 index 000000000..68d0f0744 --- /dev/null +++ b/other/nightly/docker-compose.yml @@ -0,0 +1,37 @@ +services: + coolify: + container_name: coolify + restart: always + working_dir: /var/www/html + extra_hosts: + - 'host.docker.internal:host-gateway' + networks: + - coolify + depends_on: + - postgres + - redis + - soketi + postgres: + image: postgres:15-alpine + container_name: coolify-db + restart: always + networks: + - coolify + redis: + image: redis:alpine + container_name: coolify-redis + restart: always + networks: + - coolify + soketi: + container_name: coolify-realtime + extra_hosts: + - 'host.docker.internal:host-gateway' + restart: always + networks: + - coolify +networks: + coolify: + name: coolify + driver: bridge + external: false diff --git a/other/nightly/install.sh b/other/nightly/install.sh new file mode 100755 index 000000000..2371cca2c --- /dev/null +++ b/other/nightly/install.sh @@ -0,0 +1,496 @@ +#!/bin/bash +## Do not modify this file. You will lose the ability to install and auto-update! + +set -e # Exit immediately if a command exits with a non-zero status +## $1 could be empty, so we need to disable this check +#set -u # Treat unset variables as an error and exit +set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status +CDN="https://cdn.coollabs.io/coolify-nightly" +DATE=$(date +"%Y%m%d-%H%M%S") + +VERSION="1.6" +DOCKER_VERSION="26.0" +# TODO: Ask for a user +CURRENT_USER=$USER + +mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,sentinel} +mkdir -p /data/coolify/ssh/{keys,mux} +mkdir -p /data/coolify/proxy/dynamic + +chown -R 9999:root /data/coolify +chmod -R 700 /data/coolify + +INSTALLATION_LOG_WITH_DATE="/data/coolify/source/installation-${DATE}.log" + +exec > >(tee -a $INSTALLATION_LOG_WITH_DATE) 2>&1 + +getAJoke() { + JOKES=$(curl -s --max-time 2 "https://v2.jokeapi.dev/joke/Programming?blacklistFlags=nsfw,religious,political,racist,sexist,explicit&format=txt&type=single" || true) + if [ "$JOKES" != "" ]; then + echo -e " - Until then, here's a joke for you:\n" + echo -e "$JOKES\n" + fi +} +OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"') +ENV_FILE="/data/coolify/source/.env" + +# Check if the OS is manjaro, if so, change it to arch +if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then + OS_TYPE="arch" +fi + +# Check if the OS is Asahi Linux, if so, change it to fedora +if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then + OS_TYPE="fedora" +fi + +# Check if the OS is popOS, if so, change it to ubuntu +if [ "$OS_TYPE" = "pop" ]; then + OS_TYPE="ubuntu" +fi + +# Check if the OS is linuxmint, if so, change it to ubuntu +if [ "$OS_TYPE" = "linuxmint" ]; then + OS_TYPE="ubuntu" +fi + +#Check if the OS is zorin, if so, change it to ubuntu +if [ "$OS_TYPE" = "zorin" ]; then + OS_TYPE="ubuntu" +fi + +if [ "$OS_TYPE" = "arch" ] || [ "$OS_TYPE" = "archarm" ]; then + OS_VERSION="rolling" +else + OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"') +fi + +# Install xargs on Amazon Linux 2023 - lol +if [ "$OS_TYPE" = 'amzn' ]; then + dnf install -y findutils >/dev/null +fi + +LATEST_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $2}' | tr -d ',') +LATEST_HELPER_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $6}' | tr -d ',') +LATEST_REALTIME_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $8}' | tr -d ',') + +if [ -z "$LATEST_HELPER_VERSION" ]; then + LATEST_HELPER_VERSION=latest +fi + +if [ -z "$LATEST_REALTIME_VERSION" ]; then + LATEST_REALTIME_VERSION=latest +fi + + +if [ $EUID != 0 ]; then + echo "Please run as root" + exit +fi + +case "$OS_TYPE" in +arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;; +*) + echo "This script only supports Debian, Redhat, Arch Linux, Alpine Linux, or SLES based operating systems for now." + exit + ;; +esac + +# Overwrite LATEST_VERSION if user pass a version number +if [ "$1" != "" ]; then + LATEST_VERSION=$1 + LATEST_VERSION="${LATEST_VERSION,,}" + LATEST_VERSION="${LATEST_VERSION#v}" +fi + +echo -e "\033[0;35m" +cat << "EOF" + _____ _ _ __ + / ____| | (_)/ _| + | | ___ ___ | |_| |_ _ _ + | | / _ \ / _ \| | | _| | | | + | |___| (_) | (_) | | | | | |_| | + \_____\___/ \___/|_|_|_| \__, | + __/ | + |___/ +EOF +echo -e "\033[0m" +echo -e "Welcome to Coolify Installer!" +echo -e "This script will install everything for you. Sit back and relax." +echo -e "Source code: https://github.com/coollabsio/coolify/blob/main/scripts/install.sh\n" +echo -e "---------------------------------------------" +echo "| Operating System | $OS_TYPE $OS_VERSION" +echo "| Docker | $DOCKER_VERSION" +echo "| Coolify | $LATEST_VERSION" +echo "| Helper | $LATEST_HELPER_VERSION" +echo "| Realtime | $LATEST_REALTIME_VERSION" +echo -e "---------------------------------------------\n" +echo -e "1. Installing required packages (curl, wget, git, jq). " + +case "$OS_TYPE" in +arch) + pacman -Sy --noconfirm --needed curl wget git jq >/dev/null || true + ;; +alpine) + sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories + apk update >/dev/null + apk add curl wget git jq >/dev/null + ;; +ubuntu | debian | raspbian) + apt-get update -y >/dev/null + apt-get install -y curl wget git jq >/dev/null + ;; +centos | fedora | rhel | ol | rocky | almalinux | amzn) + if [ "$OS_TYPE" = "amzn" ]; then + dnf install -y wget git jq >/dev/null + else + if ! command -v dnf >/dev/null; then + yum install -y dnf >/dev/null + fi + if ! command -v curl >/dev/null; then + dnf install -y curl >/dev/null + fi + dnf install -y wget git jq >/dev/null + fi + ;; +sles | opensuse-leap | opensuse-tumbleweed) + zypper refresh >/dev/null + zypper install -y curl wget git jq >/dev/null + ;; +*) + echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now." + exit + ;; +esac + + +echo -e "2. Check OpenSSH server configuration. " + +# Detect OpenSSH server +SSH_DETECTED=false +if [ -x "$(command -v systemctl)" ]; then + if systemctl status sshd >/dev/null 2>&1; then + echo " - OpenSSH server is installed." + SSH_DETECTED=true + elif systemctl status ssh >/dev/null 2>&1; then + echo " - OpenSSH server is installed." + SSH_DETECTED=true + fi +elif [ -x "$(command -v service)" ]; then + if service sshd status >/dev/null 2>&1; then + echo " - OpenSSH server is installed." + SSH_DETECTED=true + elif service ssh status >/dev/null 2>&1; then + echo " - OpenSSH server is installed." + SSH_DETECTED=true + fi +fi + + +if [ "$SSH_DETECTED" = "false" ]; then + echo " - OpenSSH server not detected. Installing OpenSSH server." + case "$OS_TYPE" in + arch) + pacman -Sy --noconfirm openssh >/dev/null + systemctl enable sshd >/dev/null 2>&1 + systemctl start sshd >/dev/null 2>&1 + ;; + alpine) + apk add openssh >/dev/null + rc-update add sshd default >/dev/null 2>&1 + service sshd start >/dev/null 2>&1 + ;; + ubuntu | debian | raspbian) + apt-get update -y >/dev/null + apt-get install -y openssh-server >/dev/null + systemctl enable ssh >/dev/null 2>&1 + systemctl start ssh >/dev/null 2>&1 + ;; + centos | fedora | rhel | ol | rocky | almalinux | amzn) + if [ "$OS_TYPE" = "amzn" ]; then + dnf install -y openssh-server >/dev/null + else + dnf install -y openssh-server >/dev/null + fi + systemctl enable sshd >/dev/null 2>&1 + systemctl start sshd >/dev/null 2>&1 + ;; + sles | opensuse-leap | opensuse-tumbleweed) + zypper install -y openssh >/dev/null + systemctl enable sshd >/dev/null 2>&1 + systemctl start sshd >/dev/null 2>&1 + ;; + *) + echo "###############################################################################" + echo "WARNING: Could not detect and install OpenSSH server - this does not mean that it is not installed or not running, just that we could not detect it." + echo -e "Please make sure it is installed and running, otherwise Coolify cannot connect to the host system. \n" + echo "###############################################################################" + exit 1 + ;; + esac + echo " - OpenSSH server installed successfully." + SSH_DETECTED=true +fi + +# Detect SSH PermitRootLogin +SSH_PERMIT_ROOT_LOGIN=$(sshd -T | grep -i "permitrootlogin" | awk '{print $2}') || true +if [ "$SSH_PERMIT_ROOT_LOGIN" = "yes" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "without-password" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "prohibit-password" ]; then + echo " - SSH PermitRootLogin is enabled." +else + echo " - SSH PermitRootLogin is disabled." + echo " If you have problems with SSH, please read this: https://coolify.io/docs/knowledge-base/server/openssh" +fi + +# Detect if docker is installed via snap +if [ -x "$(command -v snap)" ]; then + SNAP_DOCKER_INSTALLED=$(snap list docker >/dev/null 2>&1 && echo "true" || echo "false") + if [ "$SNAP_DOCKER_INSTALLED" = "true" ]; then + echo " - Docker is installed via snap." + echo " Please note that Coolify does not support Docker installed via snap." + echo " Please remove Docker with snap (snap remove docker) and reexecute this script." + exit 1 + fi +fi + +echo -e "3. Check Docker Installation. " +if ! [ -x "$(command -v docker)" ]; then + echo " - Docker is not installed. Installing Docker. It may take a while." + getAJoke + case "$OS_TYPE" in + "almalinux") + dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1 + dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1 + if ! [ -x "$(command -v docker)" ]; then + echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." + exit 1 + fi + systemctl start docker >/dev/null 2>&1 + systemctl enable docker >/dev/null 2>&1 + ;; + "alpine") + apk add docker docker-cli-compose >/dev/null 2>&1 + rc-update add docker default >/dev/null 2>&1 + service docker start >/dev/null 2>&1 + if ! [ -x "$(command -v docker)" ]; then + echo " - Failed to install Docker with apk. Try to install it manually." + echo " Please visit https://wiki.alpinelinux.org/wiki/Docker for more information." + exit 1 + fi + ;; + "arch") + pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1 + systemctl enable docker.service >/dev/null 2>&1 + if ! [ -x "$(command -v docker)" ]; then + echo " - Failed to install Docker with pacman. Try to install it manually." + echo " Please visit https://wiki.archlinux.org/title/docker for more information." + exit 1 + fi + ;; + "amzn") + dnf install docker -y >/dev/null 2>&1 + DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker} + mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1 + curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1 + chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1 + systemctl start docker >/dev/null 2>&1 + systemctl enable docker >/dev/null 2>&1 + if ! [ -x "$(command -v docker)" ]; then + echo " - Failed to install Docker with dnf. Try to install it manually." + echo " Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information." + exit 1 + fi + ;; + *) + if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then + echo "Docker automated installation is not supported on Ubuntu 24.10 (non-LTS release)." + echo "Please install Docker manually." + exit 1 + fi + curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh 2>&1 + if ! [ -x "$(command -v docker)" ]; then + curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} 2>&1 + if ! [ -x "$(command -v docker)" ]; then + echo " - Docker installation failed." + echo " Maybe your OS is not supported?" + echo " - Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." + exit 1 + fi + fi + esac + echo " - Docker installed successfully." +else + echo " - Docker is installed." +fi + +echo -e "4. Check Docker Configuration. " +mkdir -p /etc/docker +# shellcheck disable=SC2015 +test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json /etc/docker/daemon.json.original-"$DATE" || cat >/etc/docker/daemon.json </etc/docker/daemon.json.coolify <"$TEMP_FILE"; then + echo "Error merging JSON files" + exit 1 +fi +mv "$TEMP_FILE" /etc/docker/daemon.json + +restart_docker_service() { + # Check if systemctl is available + if command -v systemctl >/dev/null 2>&1; then + echo " - Using systemctl to restart Docker." + systemctl restart docker + + if [ $? -eq 0 ]; then + echo " - Docker restarted successfully using systemctl." + else + echo " - Failed to restart Docker using systemctl." + return 1 + fi + + # Check if service command is available + elif command -v service >/dev/null 2>&1; then + echo " - Using service command to restart Docker." + service docker restart + + if [ $? -eq 0 ]; then + echo " - Docker restarted successfully using service." + else + echo " - Failed to restart Docker using service." + return 1 + fi + + # If neither systemctl nor service is available + else + echo " - Neither systemctl nor service command is available on this system." + return 1 + fi +} + +if [ -s /etc/docker/daemon.json.original-"$DATE" ]; then + DIFF=$(diff <(jq --sort-keys . /etc/docker/daemon.json) <(jq --sort-keys . /etc/docker/daemon.json.original-"$DATE")) + if [ "$DIFF" != "" ]; then + echo " - Docker configuration updated, restart docker daemon..." + restart_docker_service + else + echo " - Docker configuration is up to date." + fi +else + echo " - Docker configuration updated, restart docker daemon..." + restart_docker_service +fi + +echo -e "5. Download required files from CDN. " +curl -fsSL $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml +curl -fsSL $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml +curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production +curl -fsSL $CDN/upgrade.sh -o /data/coolify/source/upgrade.sh + +echo -e "6. Make backup of .env to .env-$DATE" + +# Copy .env.example if .env does not exist +if [ -f $ENV_FILE ]; then + cp $ENV_FILE $ENV_FILE-$DATE +else + echo " - File does not exist: $ENV_FILE" + echo " - Copying .env.production to .env-$DATE" + cp /data/coolify/source/.env.production $ENV_FILE-$DATE + # Generate a secure APP_ID and APP_KEY + sed -i "s|^APP_ID=.*|APP_ID=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE" + sed -i "s|^APP_KEY=.*|APP_KEY=base64:$(openssl rand -base64 32)|" "$ENV_FILE-$DATE" + + # Generate a secure Postgres DB username and password + # Causes issues: database "random-user" does not exist + # sed -i "s|^DB_USERNAME=.*|DB_USERNAME=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE" + sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=$(openssl rand -base64 32)|" "$ENV_FILE-$DATE" + + # Generate a secure Redis password + sed -i "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=$(openssl rand -base64 32)|" "$ENV_FILE-$DATE" + + # Generate secure Pusher credentials + sed -i "s|^PUSHER_APP_ID=.*|PUSHER_APP_ID=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE" + sed -i "s|^PUSHER_APP_KEY=.*|PUSHER_APP_KEY=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE" + sed -i "s|^PUSHER_APP_SECRET=.*|PUSHER_APP_SECRET=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE" +fi + +# Merge .env and .env.production. New values will be added to .env +echo -e "7. Propagating .env with new values - if necessary." +awk -F '=' '!seen[$1]++' "$ENV_FILE-$DATE" /data/coolify/source/.env.production > $ENV_FILE + +if [ "$AUTOUPDATE" = "false" ]; then + if ! grep -q "AUTOUPDATE=" /data/coolify/source/.env; then + echo "AUTOUPDATE=false" >>/data/coolify/source/.env + else + sed -i "s|AUTOUPDATE=.*|AUTOUPDATE=false|g" /data/coolify/source/.env + fi +fi +echo -e "8. Checking for SSH key for localhost access." +if [ ! -f ~/.ssh/authorized_keys ]; then + mkdir -p ~/.ssh + chmod 700 ~/.ssh + touch ~/.ssh/authorized_keys + chmod 600 ~/.ssh/authorized_keys +fi + +set +e +IS_COOLIFY_VOLUME_EXISTS=$(docker volume ls | grep coolify-db | wc -l) +set -e + +if [ "$IS_COOLIFY_VOLUME_EXISTS" -eq 0 ]; then + echo " - Generating SSH key." + ssh-keygen -t ed25519 -a 100 -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal -q -N "" -C coolify + chown 9999 /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal + sed -i "/coolify/d" ~/.ssh/authorized_keys + cat /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub >> ~/.ssh/authorized_keys + rm -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub +fi + +chown -R 9999:root /data/coolify +chmod -R 700 /data/coolify + +echo -e "9. Installing Coolify ($LATEST_VERSION)" +echo -e " - It could take a while based on your server's performance, network speed, stars, etc." +echo -e " - Please wait." +getAJoke + +bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" +echo " - Coolify installed successfully." +rm -f $ENV_FILE-$DATE + +echo " - Waiting for 20 seconds for Coolify (database migrations) to be ready." +getAJoke + +sleep 20 +echo -e "\033[0;35m + ____ _ _ _ _ _ + / ___|___ _ __ __ _ _ __ __ _| |_ _ _| | __ _| |_(_) ___ _ __ ___| | + | | / _ \| '_ \ / _\` | '__/ _\` | __| | | | |/ _\` | __| |/ _ \| '_ \/ __| | + | |__| (_) | | | | (_| | | | (_| | |_| |_| | | (_| | |_| | (_) | | | \__ \_| + \____\___/|_| |_|\__, |_| \__,_|\__|\__,_|_|\__,_|\__|_|\___/|_| |_|___(_) + |___/ +\033[0m" +echo -e "\nYour instance is ready to use." +echo -e "Please visit http://$(curl -4s https://ifconfig.io):8000 to get started.\n" +echo -e "WARNING: We recommend you to backup your /data/coolify/source/.env file to a safe location, outside of this server." +cp /data/coolify/source/.env /data/coolify/source/.env.backup diff --git a/other/nightly/upgrade.sh b/other/nightly/upgrade.sh new file mode 100644 index 000000000..670072b12 --- /dev/null +++ b/other/nightly/upgrade.sh @@ -0,0 +1,41 @@ +#!/bin/bash +## Do not modify this file. You will lose the ability to autoupdate! + +VERSION="13" +CDN="https://cdn.coollabs.io/coolify-nightly" +LATEST_IMAGE=${1:-latest} +LATEST_HELPER_VERSION=${2:-latest} + +DATE=$(date +%Y-%m-%d-%H-%M-%S) +LOGFILE="/data/coolify/source/upgrade-${DATE}.log" + +curl -fsSL $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml +curl -fsSL $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml +curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production + +# Merge .env and .env.production. New values will be added to .env +awk -F '=' '!seen[$1]++' /data/coolify/source/.env /data/coolify/source/.env.production > /data/coolify/source/.env.tmp && mv /data/coolify/source/.env.tmp /data/coolify/source/.env +# Check if PUSHER_APP_ID or PUSHER_APP_KEY or PUSHER_APP_SECRET is empty in /data/coolify/source/.env +if grep -q "PUSHER_APP_ID=$" /data/coolify/source/.env; then + sed -i "s|PUSHER_APP_ID=.*|PUSHER_APP_ID=$(openssl rand -hex 32)|g" /data/coolify/source/.env +fi + +if grep -q "PUSHER_APP_KEY=$" /data/coolify/source/.env; then + sed -i "s|PUSHER_APP_KEY=.*|PUSHER_APP_KEY=$(openssl rand -hex 32)|g" /data/coolify/source/.env +fi + +if grep -q "PUSHER_APP_SECRET=$" /data/coolify/source/.env; then + sed -i "s|PUSHER_APP_SECRET=.*|PUSHER_APP_SECRET=$(openssl rand -hex 32)|g" /data/coolify/source/.env +fi + +# Make sure coolify network exists +# It is created when starting Coolify with docker compose +docker network create --attachable coolify 2>/dev/null +# docker network create --attachable --driver=overlay coolify-overlay 2>/dev/null + +if [ -f /data/coolify/source/docker-compose.custom.yml ]; then + echo "docker-compose.custom.yml detected." >> $LOGFILE + docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >> $LOGFILE 2>&1 +else + docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >> $LOGFILE 2>&1 +fi diff --git a/other/nightly/versions.json b/other/nightly/versions.json new file mode 100644 index 000000000..8b10875d0 --- /dev/null +++ b/other/nightly/versions.json @@ -0,0 +1,19 @@ +{ + "coolify": { + "v4": { + "version": "4.0.0-beta.367" + }, + "nightly": { + "version": "4.0.0-beta.368" + }, + "helper": { + "version": "1.0.3" + }, + "realtime": { + "version": "1.0.4" + }, + "sentinel": { + "version": "0.0.15" + } + } +} \ No newline at end of file diff --git a/other/scripts/get-subs.php b/other/scripts/get-subs.php deleted file mode 100644 index 3a23fc073..000000000 --- a/other/scripts/get-subs.php +++ /dev/null @@ -1,11 +0,0 @@ -$handle = fopen("/tmp/export.csv", "w"); -App\Models\Team::chunk(100, function ($teams) use ($handle) { - foreach ($teams as $team) { - if ($team->subscription->stripe_invoice_paid == true) { - foreach ($team->members as $member) { - fputcsv($handle, [$member->email, $member->name], ","); - } - } - } -}); -fclose($handle); diff --git a/package-lock.json b/package-lock.json index 0010d87fa..adb1dc65a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,21 +7,27 @@ "dependencies": { "@tailwindcss/forms": "0.5.7", "@tailwindcss/typography": "0.5.13", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", "alpinejs": "3.14.0", + "cookie": "^0.7.0", + "dotenv": "^16.4.5", "ioredis": "5.4.1", - "tailwindcss-scrollbar": "0.1.0" + "node-pty": "^1.0.0", + "tailwindcss-scrollbar": "0.1.0", + "ws": "^8.17.0" }, "devDependencies": { "@vitejs/plugin-vue": "4.5.1", "autoprefixer": "10.4.19", - "axios": "1.7.2", + "axios": "1.7.5", "laravel-echo": "1.16.1", "laravel-vite-plugin": "0.8.1", "postcss": "8.4.38", "pusher-js": "8.4.0-rc2", - "tailwindcss": "3.4.3", - "vite": "4.5.3", - "vue": "3.4.27" + "tailwindcss": "3.4.4", + "vite": "4.5.5", + "vue": "3.4.29" } }, "node_modules/@alloc/quick-lru": { @@ -36,9 +42,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -535,51 +541,51 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.27.tgz", - "integrity": "sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.29.tgz", + "integrity": "sha512-TFKiRkKKsRCKvg/jTSSKK7mYLJEQdUiUfykbG49rubC9SfDyvT2JrzTReopWlz2MxqeLyxh9UZhvxEIBgAhtrg==", "dev": true, "dependencies": { - "@babel/parser": "^7.24.4", - "@vue/shared": "3.4.27", + "@babel/parser": "^7.24.7", + "@vue/shared": "3.4.29", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-core/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/@vue/compiler-dom": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.27.tgz", - "integrity": "sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.29.tgz", + "integrity": "sha512-A6+iZ2fKIEGnfPJejdB7b1FlJzgiD+Y/sxxKwJWg1EbJu6ZPgzaPQQ51ESGNv0CP6jm6Z7/pO6Ia8Ze6IKrX7w==", "dev": true, "dependencies": { - "@vue/compiler-core": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/compiler-core": "3.4.29", + "@vue/shared": "3.4.29" } }, "node_modules/@vue/compiler-dom/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/@vue/compiler-sfc": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.27.tgz", - "integrity": "sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.29.tgz", + "integrity": "sha512-zygDcEtn8ZimDlrEQyLUovoWgKQic6aEQqRXce2WXBvSeHbEbcAsXyCk9oG33ZkyWH4sl9D3tkYc1idoOkdqZQ==", "dev": true, "dependencies": { - "@babel/parser": "^7.24.4", - "@vue/compiler-core": "3.4.27", - "@vue/compiler-dom": "3.4.27", - "@vue/compiler-ssr": "3.4.27", - "@vue/shared": "3.4.27", + "@babel/parser": "^7.24.7", + "@vue/compiler-core": "3.4.29", + "@vue/compiler-dom": "3.4.29", + "@vue/compiler-ssr": "3.4.29", + "@vue/shared": "3.4.29", "estree-walker": "^2.0.2", "magic-string": "^0.30.10", "postcss": "^8.4.38", @@ -587,25 +593,25 @@ } }, "node_modules/@vue/compiler-sfc/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/@vue/compiler-ssr": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.27.tgz", - "integrity": "sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.29.tgz", + "integrity": "sha512-rFbwCmxJ16tDp3N8XCx5xSQzjhidYjXllvEcqX/lopkoznlNPz3jyy0WGJCyhAaVQK677WWFt3YO/WUEkMMUFQ==", "dev": true, "dependencies": { - "@vue/compiler-dom": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/compiler-dom": "3.4.29", + "@vue/shared": "3.4.29" } }, "node_modules/@vue/compiler-ssr/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/@vue/reactivity": { @@ -617,64 +623,74 @@ } }, "node_modules/@vue/runtime-core": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.27.tgz", - "integrity": "sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.29.tgz", + "integrity": "sha512-s8fmX3YVR/Rk5ig0ic0NuzTNjK2M7iLuVSZyMmCzN/+Mjuqqif1JasCtEtmtoJWF32pAtUjyuT2ljNKNLeOmnQ==", "dev": true, "dependencies": { - "@vue/reactivity": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/reactivity": "3.4.29", + "@vue/shared": "3.4.29" } }, "node_modules/@vue/runtime-core/node_modules/@vue/reactivity": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.27.tgz", - "integrity": "sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.29.tgz", + "integrity": "sha512-w8+KV+mb1a8ornnGQitnMdLfE0kXmteaxLdccm2XwdFxXst4q/Z7SEboCV5SqJNpZbKFeaRBBJBhW24aJyGINg==", "dev": true, "dependencies": { - "@vue/shared": "3.4.27" + "@vue/shared": "3.4.29" } }, "node_modules/@vue/runtime-core/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/@vue/runtime-dom": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.27.tgz", - "integrity": "sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.29.tgz", + "integrity": "sha512-gI10atCrtOLf/2MPPMM+dpz3NGulo9ZZR9d1dWo4fYvm+xkfvRrw1ZmJ7mkWtiJVXSsdmPbcK1p5dZzOCKDN0g==", "dev": true, "dependencies": { - "@vue/runtime-core": "3.4.27", - "@vue/shared": "3.4.27", + "@vue/reactivity": "3.4.29", + "@vue/runtime-core": "3.4.29", + "@vue/shared": "3.4.29", "csstype": "^3.1.3" } }, + "node_modules/@vue/runtime-dom/node_modules/@vue/reactivity": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.29.tgz", + "integrity": "sha512-w8+KV+mb1a8ornnGQitnMdLfE0kXmteaxLdccm2XwdFxXst4q/Z7SEboCV5SqJNpZbKFeaRBBJBhW24aJyGINg==", + "dev": true, + "dependencies": { + "@vue/shared": "3.4.29" + } + }, "node_modules/@vue/runtime-dom/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/@vue/server-renderer": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.27.tgz", - "integrity": "sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.29.tgz", + "integrity": "sha512-HMLCmPI2j/k8PVkSBysrA2RxcxC5DgBiCdj7n7H2QtR8bQQPqKAe8qoaxLcInzouBmzwJ+J0x20ygN/B5mYBng==", "dev": true, "dependencies": { - "@vue/compiler-ssr": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/compiler-ssr": "3.4.29", + "@vue/shared": "3.4.29" }, "peerDependencies": { - "vue": "3.4.27" + "vue": "3.4.29" } }, "node_modules/@vue/server-renderer/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/@vue/shared": { @@ -682,6 +698,19 @@ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==" }, + "node_modules/@xterm/addon-fit": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", + "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, + "node_modules/@xterm/xterm": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==" + }, "node_modules/alpinejs": { "version": "3.14.0", "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.0.tgz", @@ -756,10 +785,11 @@ } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz", + "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==", "dev": true, + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -789,11 +819,11 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -929,6 +959,14 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/cookie": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.0.tgz", + "integrity": "sha512-qCf+V4dtlNhSRXGAZatc1TasyFO6GjohcOul807YOb5ik3+kQSnb4d7iajeCL8QHaJ4uZEjCgiCJerKXwdRVlQ==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -989,6 +1027,18 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.692", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.692.tgz", @@ -1094,9 +1144,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1398,11 +1448,11 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -1464,6 +1514,11 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==" + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -1481,6 +1536,15 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-pty": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", + "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", + "hasInstallScript": true, + "dependencies": { + "nan": "^2.17.0" + } + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -1795,9 +1859,9 @@ } }, "node_modules/rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -1897,9 +1961,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", - "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", + "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -2017,9 +2081,9 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/vite": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", - "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz", + "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==", "dev": true, "dependencies": { "esbuild": "^0.18.10", @@ -2082,16 +2146,16 @@ } }, "node_modules/vue": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.27.tgz", - "integrity": "sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.29.tgz", + "integrity": "sha512-8QUYfRcYzNlYuzKPfge1UWC6nF9ym0lx7mpGVPJYNhddxEf3DD0+kU07NTL0sXuiT2HuJuKr/iEO8WvXvT0RSQ==", "dev": true, "dependencies": { - "@vue/compiler-dom": "3.4.27", - "@vue/compiler-sfc": "3.4.27", - "@vue/runtime-dom": "3.4.27", - "@vue/server-renderer": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/compiler-dom": "3.4.29", + "@vue/compiler-sfc": "3.4.29", + "@vue/runtime-dom": "3.4.29", + "@vue/server-renderer": "3.4.29", + "@vue/shared": "3.4.29" }, "peerDependencies": { "typescript": "*" @@ -2103,9 +2167,9 @@ } }, "node_modules/vue/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/wrappy": { @@ -2113,6 +2177,26 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yaml": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", diff --git a/package.json b/package.json index 4d6b321c8..29f8f1a37 100644 --- a/package.json +++ b/package.json @@ -8,20 +8,26 @@ "devDependencies": { "@vitejs/plugin-vue": "4.5.1", "autoprefixer": "10.4.19", - "axios": "1.7.2", + "axios": "1.7.5", "laravel-echo": "1.16.1", "laravel-vite-plugin": "0.8.1", "postcss": "8.4.38", "pusher-js": "8.4.0-rc2", - "tailwindcss": "3.4.3", - "vite": "4.5.3", - "vue": "3.4.27" + "tailwindcss": "3.4.4", + "vite": "4.5.5", + "vue": "3.4.29" }, "dependencies": { "@tailwindcss/forms": "0.5.7", "@tailwindcss/typography": "0.5.13", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", "alpinejs": "3.14.0", + "cookie": "^0.7.0", + "dotenv": "^16.4.5", "ioredis": "5.4.1", - "tailwindcss-scrollbar": "0.1.0" + "node-pty": "^1.0.0", + "tailwindcss-scrollbar": "0.1.0", + "ws": "^8.17.0" } -} +} \ No newline at end of file diff --git a/public/js/apexcharts.js b/public/js/apexcharts.js new file mode 100644 index 000000000..c1353e834 --- /dev/null +++ b/public/js/apexcharts.js @@ -0,0 +1,16 @@ +/*! + * ApexCharts v3.49.1 + * (c) 2018-2024 ApexCharts + * Released under the MIT License. + */ +!function (t, e) { "object" == typeof exports && "undefined" != typeof module ? module.exports = e() : "function" == typeof define && define.amd ? define(e) : (t = "undefined" != typeof globalThis ? globalThis : t || self).ApexCharts = e() }(this, (function () { + "use strict"; function t(t, e) { var i = Object.keys(t); if (Object.getOwnPropertySymbols) { var a = Object.getOwnPropertySymbols(t); e && (a = a.filter((function (e) { return Object.getOwnPropertyDescriptor(t, e).enumerable }))), i.push.apply(i, a) } return i } function e(e) { for (var i = 1; i < arguments.length; i++) { var a = null != arguments[i] ? arguments[i] : {}; i % 2 ? t(Object(a), !0).forEach((function (t) { o(e, t, a[t]) })) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(a)) : t(Object(a)).forEach((function (t) { Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(a, t)) })) } return e } function i(t) { return i = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (t) { return typeof t } : function (t) { return t && "function" == typeof Symbol && t.constructor === Symbol && t !== Symbol.prototype ? "symbol" : typeof t }, i(t) } function a(t, e) { if (!(t instanceof e)) throw new TypeError("Cannot call a class as a function") } function s(t, e) { for (var i = 0; i < e.length; i++) { var a = e[i]; a.enumerable = a.enumerable || !1, a.configurable = !0, "value" in a && (a.writable = !0), Object.defineProperty(t, a.key, a) } } function r(t, e, i) { return e && s(t.prototype, e), i && s(t, i), t } function o(t, e, i) { return e in t ? Object.defineProperty(t, e, { value: i, enumerable: !0, configurable: !0, writable: !0 }) : t[e] = i, t } function n(t, e) { if ("function" != typeof e && null !== e) throw new TypeError("Super expression must either be null or a function"); t.prototype = Object.create(e && e.prototype, { constructor: { value: t, writable: !0, configurable: !0 } }), e && h(t, e) } function l(t) { return l = Object.setPrototypeOf ? Object.getPrototypeOf : function (t) { return t.__proto__ || Object.getPrototypeOf(t) }, l(t) } function h(t, e) { return h = Object.setPrototypeOf || function (t, e) { return t.__proto__ = e, t }, h(t, e) } function c(t) { if (void 0 === t) throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); return t } function d(t) { var e = function () { if ("undefined" == typeof Reflect || !Reflect.construct) return !1; if (Reflect.construct.sham) return !1; if ("function" == typeof Proxy) return !0; try { return Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], (function () { }))), !0 } catch (t) { return !1 } }(); return function () { var i, a = l(t); if (e) { var s = l(this).constructor; i = Reflect.construct(a, arguments, s) } else i = a.apply(this, arguments); return function (t, e) { if (e && ("object" == typeof e || "function" == typeof e)) return e; if (void 0 !== e) throw new TypeError("Derived constructors may only return object or undefined"); return c(t) }(this, i) } } function g(t, e) { return function (t) { if (Array.isArray(t)) return t }(t) || function (t, e) { var i = null == t ? null : "undefined" != typeof Symbol && t[Symbol.iterator] || t["@@iterator"]; if (null == i) return; var a, s, r = [], o = !0, n = !1; try { for (i = i.call(t); !(o = (a = i.next()).done) && (r.push(a.value), !e || r.length !== e); o = !0); } catch (t) { n = !0, s = t } finally { try { o || null == i.return || i.return() } finally { if (n) throw s } } return r }(t, e) || p(t, e) || function () { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.") }() } function u(t) { return function (t) { if (Array.isArray(t)) return f(t) }(t) || function (t) { if ("undefined" != typeof Symbol && null != t[Symbol.iterator] || null != t["@@iterator"]) return Array.from(t) }(t) || p(t) || function () { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.") }() } function p(t, e) { if (t) { if ("string" == typeof t) return f(t, e); var i = Object.prototype.toString.call(t).slice(8, -1); return "Object" === i && t.constructor && (i = t.constructor.name), "Map" === i || "Set" === i ? Array.from(t) : "Arguments" === i || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i) ? f(t, e) : void 0 } } function f(t, e) { (null == e || e > t.length) && (e = t.length); for (var i = 0, a = new Array(e); i < e; i++)a[i] = t[i]; return a } var x = function () { function t() { a(this, t) } return r(t, [{ key: "shadeRGBColor", value: function (t, e) { var i = e.split(","), a = t < 0 ? 0 : 255, s = t < 0 ? -1 * t : t, r = parseInt(i[0].slice(4), 10), o = parseInt(i[1], 10), n = parseInt(i[2], 10); return "rgb(" + (Math.round((a - r) * s) + r) + "," + (Math.round((a - o) * s) + o) + "," + (Math.round((a - n) * s) + n) + ")" } }, { key: "shadeHexColor", value: function (t, e) { var i = parseInt(e.slice(1), 16), a = t < 0 ? 0 : 255, s = t < 0 ? -1 * t : t, r = i >> 16, o = i >> 8 & 255, n = 255 & i; return "#" + (16777216 + 65536 * (Math.round((a - r) * s) + r) + 256 * (Math.round((a - o) * s) + o) + (Math.round((a - n) * s) + n)).toString(16).slice(1) } }, { key: "shadeColor", value: function (e, i) { return t.isColorHex(i) ? this.shadeHexColor(e, i) : this.shadeRGBColor(e, i) } }], [{ key: "bind", value: function (t, e) { return function () { return t.apply(e, arguments) } } }, { key: "isObject", value: function (t) { return t && "object" === i(t) && !Array.isArray(t) && null != t } }, { key: "is", value: function (t, e) { return Object.prototype.toString.call(e) === "[object " + t + "]" } }, { key: "listToArray", value: function (t) { var e, i = []; for (e = 0; e < t.length; e++)i[e] = t[e]; return i } }, { key: "extend", value: function (t, e) { var i = this; "function" != typeof Object.assign && (Object.assign = function (t) { if (null == t) throw new TypeError("Cannot convert undefined or null to object"); for (var e = Object(t), i = 1; i < arguments.length; i++) { var a = arguments[i]; if (null != a) for (var s in a) a.hasOwnProperty(s) && (e[s] = a[s]) } return e }); var a = Object.assign({}, t); return this.isObject(t) && this.isObject(e) && Object.keys(e).forEach((function (s) { i.isObject(e[s]) && s in t ? a[s] = i.extend(t[s], e[s]) : Object.assign(a, o({}, s, e[s])) })), a } }, { key: "extendArray", value: function (e, i) { var a = []; return e.map((function (e) { a.push(t.extend(i, e)) })), e = a } }, { key: "monthMod", value: function (t) { return t % 12 } }, { key: "clone", value: function (e) { if (t.is("Array", e)) { for (var a = [], s = 0; s < e.length; s++)a[s] = this.clone(e[s]); return a } if (t.is("Null", e)) return null; if (t.is("Date", e)) return e; if ("object" === i(e)) { var r = {}; for (var o in e) e.hasOwnProperty(o) && (r[o] = this.clone(e[o])); return r } return e } }, { key: "log10", value: function (t) { return Math.log(t) / Math.LN10 } }, { key: "roundToBase10", value: function (t) { return Math.pow(10, Math.floor(Math.log10(t))) } }, { key: "roundToBase", value: function (t, e) { return Math.pow(e, Math.floor(Math.log(t) / Math.log(e))) } }, { key: "parseNumber", value: function (t) { return null === t ? t : parseFloat(t) } }, { key: "stripNumber", value: function (t) { var e = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 2; return Number.isInteger(t) ? t : parseFloat(t.toPrecision(e)) } }, { key: "randomId", value: function () { return (Math.random() + 1).toString(36).substring(4) } }, { key: "noExponents", value: function (t) { var e = String(t).split(/[eE]/); if (1 === e.length) return e[0]; var i = "", a = t < 0 ? "-" : "", s = e[0].replace(".", ""), r = Number(e[1]) + 1; if (r < 0) { for (i = a + "0."; r++;)i += "0"; return i + s.replace(/^-/, "") } for (r -= s.length; r--;)i += "0"; return s + i } }, { key: "getDimensions", value: function (t) { var e = getComputedStyle(t, null), i = t.clientHeight, a = t.clientWidth; return i -= parseFloat(e.paddingTop) + parseFloat(e.paddingBottom), [a -= parseFloat(e.paddingLeft) + parseFloat(e.paddingRight), i] } }, { key: "getBoundingClientRect", value: function (t) { var e = t.getBoundingClientRect(); return { top: e.top, right: e.right, bottom: e.bottom, left: e.left, width: t.clientWidth, height: t.clientHeight, x: e.left, y: e.top } } }, { key: "getLargestStringFromArr", value: function (t) { return t.reduce((function (t, e) { return Array.isArray(e) && (e = e.reduce((function (t, e) { return t.length > e.length ? t : e }))), t.length > e.length ? t : e }), 0) } }, { key: "hexToRgba", value: function () { var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "#999999", e = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : .6; "#" !== t.substring(0, 1) && (t = "#999999"); var i = t.replace("#", ""); i = i.match(new RegExp("(.{" + i.length / 3 + "})", "g")); for (var a = 0; a < i.length; a++)i[a] = parseInt(1 === i[a].length ? i[a] + i[a] : i[a], 16); return void 0 !== e && i.push(e), "rgba(" + i.join(",") + ")" } }, { key: "getOpacityFromRGBA", value: function (t) { return parseFloat(t.replace(/^.*,(.+)\)/, "$1")) } }, { key: "rgb2hex", value: function (t) { return (t = t.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i)) && 4 === t.length ? "#" + ("0" + parseInt(t[1], 10).toString(16)).slice(-2) + ("0" + parseInt(t[2], 10).toString(16)).slice(-2) + ("0" + parseInt(t[3], 10).toString(16)).slice(-2) : "" } }, { key: "isColorHex", value: function (t) { return /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)|(^#[0-9A-F]{8}$)/i.test(t) } }, { key: "getPolygonPos", value: function (t, e) { for (var i = [], a = 2 * Math.PI / e, s = 0; s < e; s++) { var r = {}; r.x = t * Math.sin(s * a), r.y = -t * Math.cos(s * a), i.push(r) } return i } }, { key: "polarToCartesian", value: function (t, e, i, a) { var s = (a - 90) * Math.PI / 180; return { x: t + i * Math.cos(s), y: e + i * Math.sin(s) } } }, { key: "escapeString", value: function (t) { var e = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : "x", i = t.toString().slice(); return i = i.replace(/[` ~!@#$%^&*()|+\=?;:'",.<>{}[\]\\/]/gi, e) } }, { key: "negToZero", value: function (t) { return t < 0 ? 0 : t } }, { key: "moveIndexInArray", value: function (t, e, i) { if (i >= t.length) for (var a = i - t.length + 1; a--;)t.push(void 0); return t.splice(i, 0, t.splice(e, 1)[0]), t } }, { key: "extractNumber", value: function (t) { return parseFloat(t.replace(/[^\d.]*/g, "")) } }, { key: "findAncestor", value: function (t, e) { for (; (t = t.parentElement) && !t.classList.contains(e);); return t } }, { key: "setELstyles", value: function (t, e) { for (var i in e) e.hasOwnProperty(i) && (t.style.key = e[i]) } }, { key: "isNumber", value: function (t) { return !isNaN(t) && parseFloat(Number(t)) === t && !isNaN(parseInt(t, 10)) } }, { key: "isFloat", value: function (t) { return Number(t) === t && t % 1 != 0 } }, { key: "isSafari", value: function () { return /^((?!chrome|android).)*safari/i.test(navigator.userAgent) } }, { key: "isFirefox", value: function () { return navigator.userAgent.toLowerCase().indexOf("firefox") > -1 } }, { key: "isIE11", value: function () { if (-1 !== window.navigator.userAgent.indexOf("MSIE") || window.navigator.appVersion.indexOf("Trident/") > -1) return !0 } }, { key: "isIE", value: function () { var t = window.navigator.userAgent, e = t.indexOf("MSIE "); if (e > 0) return parseInt(t.substring(e + 5, t.indexOf(".", e)), 10); if (t.indexOf("Trident/") > 0) { var i = t.indexOf("rv:"); return parseInt(t.substring(i + 3, t.indexOf(".", i)), 10) } var a = t.indexOf("Edge/"); return a > 0 && parseInt(t.substring(a + 5, t.indexOf(".", a)), 10) } }, { key: "getGCD", value: function (t, e) { var i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : 7, a = Math.pow(10, i - Math.floor(Math.log10(Math.max(t, e)))); for (t = Math.round(Math.abs(t) * a), e = Math.round(Math.abs(e) * a); e;) { var s = e; e = t % e, t = s } return t / a } }, { key: "getPrimeFactors", value: function (t) { for (var e = [], i = 2; t >= 2;)t % i == 0 ? (e.push(i), t /= i) : i++; return e } }, { key: "mod", value: function (t, e) { var i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : 7, a = Math.pow(10, i - Math.floor(Math.log10(Math.max(t, e)))); return (t = Math.round(Math.abs(t) * a)) % (e = Math.round(Math.abs(e) * a)) / a } }]), t }(), b = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w, this.setEasingFunctions() } return r(t, [{ key: "setEasingFunctions", value: function () { var t; if (!this.w.globals.easing) { switch (this.w.config.chart.animations.easing) { case "linear": t = "-"; break; case "easein": t = "<"; break; case "easeout": t = ">"; break; case "easeinout": default: t = "<>"; break; case "swing": t = function (t) { var e = 1.70158; return (t -= 1) * t * ((e + 1) * t + e) + 1 }; break; case "bounce": t = function (t) { return t < 1 / 2.75 ? 7.5625 * t * t : t < 2 / 2.75 ? 7.5625 * (t -= 1.5 / 2.75) * t + .75 : t < 2.5 / 2.75 ? 7.5625 * (t -= 2.25 / 2.75) * t + .9375 : 7.5625 * (t -= 2.625 / 2.75) * t + .984375 }; break; case "elastic": t = function (t) { return t === !!t ? t : Math.pow(2, -10 * t) * Math.sin((t - .075) * (2 * Math.PI) / .3) + 1 } }this.w.globals.easing = t } } }, { key: "animateLine", value: function (t, e, i, a) { t.attr(e).animate(a).attr(i) } }, { key: "animateMarker", value: function (t, e, i, a, s, r) { e || (e = 0), t.attr({ r: e, width: e, height: e }).animate(a, s).attr({ r: i, width: i.width, height: i.height }).afterAll((function () { r() })) } }, { key: "animateCircle", value: function (t, e, i, a, s) { t.attr({ r: e.r, cx: e.cx, cy: e.cy }).animate(a, s).attr({ r: i.r, cx: i.cx, cy: i.cy }) } }, { key: "animateRect", value: function (t, e, i, a, s) { t.attr(e).animate(a).attr(i).afterAll((function () { return s() })) } }, { key: "animatePathsGradually", value: function (t) { var e = t.el, i = t.realIndex, a = t.j, s = t.fill, r = t.pathFrom, o = t.pathTo, n = t.speed, l = t.delay, h = this.w, c = 0; h.config.chart.animations.animateGradually.enabled && (c = h.config.chart.animations.animateGradually.delay), h.config.chart.animations.dynamicAnimation.enabled && h.globals.dataChanged && "bar" !== h.config.chart.type && (c = 0), this.morphSVG(e, i, a, "line" !== h.config.chart.type || h.globals.comboCharts ? s : "stroke", r, o, n, l * c) } }, { key: "showDelayedElements", value: function () { this.w.globals.delayedElements.forEach((function (t) { var e = t.el; e.classList.remove("apexcharts-element-hidden"), e.classList.add("apexcharts-hidden-element-shown") })) } }, { key: "animationCompleted", value: function (t) { var e = this.w; e.globals.animationEnded || (e.globals.animationEnded = !0, this.showDelayedElements(), "function" == typeof e.config.chart.events.animationEnd && e.config.chart.events.animationEnd(this.ctx, { el: t, w: e })) } }, { key: "morphSVG", value: function (t, e, i, a, s, r, o, n) { var l = this, h = this.w; s || (s = t.attr("pathFrom")), r || (r = t.attr("pathTo")); var c = function (t) { return "radar" === h.config.chart.type && (o = 1), "M 0 ".concat(h.globals.gridHeight) }; (!s || s.indexOf("undefined") > -1 || s.indexOf("NaN") > -1) && (s = c()), (!r || r.indexOf("undefined") > -1 || r.indexOf("NaN") > -1) && (r = c()), h.globals.shouldAnimate || (o = 1), t.plot(s).animate(1, h.globals.easing, n).plot(s).animate(o, h.globals.easing, n).plot(r).afterAll((function () { x.isNumber(i) ? i === h.globals.series[h.globals.maxValsInArrayIndex].length - 2 && h.globals.shouldAnimate && l.animationCompleted(t) : "none" !== a && h.globals.shouldAnimate && (!h.globals.comboCharts && e === h.globals.series.length - 1 || h.globals.comboCharts) && l.animationCompleted(t), l.showDelayedElements() })) } }]), t }(), v = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w } return r(t, [{ key: "getDefaultFilter", value: function (t, e) { var i = this.w; t.unfilter(!0), (new window.SVG.Filter).size("120%", "180%", "-5%", "-40%"), "none" !== i.config.states.normal.filter ? this.applyFilter(t, e, i.config.states.normal.filter.type, i.config.states.normal.filter.value) : i.config.chart.dropShadow.enabled && this.dropShadow(t, i.config.chart.dropShadow, e) } }, { key: "addNormalFilter", value: function (t, e) { var i = this.w; i.config.chart.dropShadow.enabled && !t.node.classList.contains("apexcharts-marker") && this.dropShadow(t, i.config.chart.dropShadow, e) } }, { key: "addLightenFilter", value: function (t, e, i) { var a = this, s = this.w, r = i.intensity; t.unfilter(!0); new window.SVG.Filter; t.filter((function (t) { var i = s.config.chart.dropShadow; (i.enabled ? a.addShadow(t, e, i) : t).componentTransfer({ rgb: { type: "linear", slope: 1.5, intercept: r } }) })), t.filterer.node.setAttribute("filterUnits", "userSpaceOnUse"), this._scaleFilterSize(t.filterer.node) } }, { key: "addDarkenFilter", value: function (t, e, i) { var a = this, s = this.w, r = i.intensity; t.unfilter(!0); new window.SVG.Filter; t.filter((function (t) { var i = s.config.chart.dropShadow; (i.enabled ? a.addShadow(t, e, i) : t).componentTransfer({ rgb: { type: "linear", slope: r } }) })), t.filterer.node.setAttribute("filterUnits", "userSpaceOnUse"), this._scaleFilterSize(t.filterer.node) } }, { key: "applyFilter", value: function (t, e, i) { var a = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : .5; switch (i) { case "none": this.addNormalFilter(t, e); break; case "lighten": this.addLightenFilter(t, e, { intensity: a }); break; case "darken": this.addDarkenFilter(t, e, { intensity: a }) } } }, { key: "addShadow", value: function (t, e, i) { var a, s = this.w, r = i.blur, o = i.top, n = i.left, l = i.color, h = i.opacity; if ((null === (a = s.config.chart.dropShadow.enabledOnSeries) || void 0 === a ? void 0 : a.length) > 0 && -1 === s.config.chart.dropShadow.enabledOnSeries.indexOf(e)) return t; var c = t.flood(Array.isArray(l) ? l[e] : l, h).composite(t.sourceAlpha, "in").offset(n, o).gaussianBlur(r).merge(t.source); return t.blend(t.source, c) } }, { key: "dropShadow", value: function (t, e) { var i, a, s = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : 0, r = e.top, o = e.left, n = e.blur, l = e.color, h = e.opacity, c = e.noUserSpaceOnUse, d = this.w; if (t.unfilter(!0), x.isIE() && "radialBar" === d.config.chart.type) return t; if ((null === (i = d.config.chart.dropShadow.enabledOnSeries) || void 0 === i ? void 0 : i.length) > 0 && -1 === (null === (a = d.config.chart.dropShadow.enabledOnSeries) || void 0 === a ? void 0 : a.indexOf(s))) return t; return l = Array.isArray(l) ? l[s] : l, t.filter((function (t) { var e = null; e = x.isSafari() || x.isFirefox() || x.isIE() ? t.flood(l, h).composite(t.sourceAlpha, "in").offset(o, r).gaussianBlur(n) : t.flood(l, h).composite(t.sourceAlpha, "in").offset(o, r).gaussianBlur(n).merge(t.source), t.blend(t.source, e) })), c || t.filterer.node.setAttribute("filterUnits", "userSpaceOnUse"), this._scaleFilterSize(t.filterer.node), t } }, { key: "setSelectionFilter", value: function (t, e, i) { var a = this.w; if (void 0 !== a.globals.selectedDataPoints[e] && a.globals.selectedDataPoints[e].indexOf(i) > -1) { t.node.setAttribute("selected", !0); var s = a.config.states.active.filter; "none" !== s && this.applyFilter(t, e, s.type, s.value) } } }, { key: "_scaleFilterSize", value: function (t) { !function (e) { for (var i in e) e.hasOwnProperty(i) && t.setAttribute(i, e[i]) }({ width: "200%", height: "200%", x: "-50%", y: "-50%" }) } }]), t }(), m = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w } return r(t, [{ key: "roundPathCorners", value: function (t, e) { function i(t, e, i) { var s = e.x - t.x, r = e.y - t.y, o = Math.sqrt(s * s + r * r); return a(t, e, Math.min(1, i / o)) } function a(t, e, i) { return { x: t.x + (e.x - t.x) * i, y: t.y + (e.y - t.y) * i } } function s(t, e) { t.length > 2 && (t[t.length - 2] = e.x, t[t.length - 1] = e.y) } function r(t) { return { x: parseFloat(t[t.length - 2]), y: parseFloat(t[t.length - 1]) } } t.indexOf("NaN") > -1 && (t = ""); var o = t.split(/[,\s]/).reduce((function (t, e) { var i = e.match("([a-zA-Z])(.+)"); return i ? (t.push(i[1]), t.push(i[2])) : t.push(e), t }), []).reduce((function (t, e) { return parseFloat(e) == e && t.length ? t[t.length - 1].push(e) : t.push([e]), t }), []), n = []; if (o.length > 1) { var l = r(o[0]), h = null; "Z" == o[o.length - 1][0] && o[0].length > 2 && (h = ["L", l.x, l.y], o[o.length - 1] = h), n.push(o[0]); for (var c = 1; c < o.length; c++) { var d = n[n.length - 1], g = o[c], u = g == h ? o[1] : o[c + 1]; if (u && d && d.length > 2 && "L" == g[0] && u.length > 2 && "L" == u[0]) { var p, f, x = r(d), b = r(g), v = r(u); p = i(b, x, e), f = i(b, v, e), s(g, p), g.origPoint = b, n.push(g); var m = a(p, b, .5), y = a(b, f, .5), w = ["C", m.x, m.y, y.x, y.y, f.x, f.y]; w.origPoint = b, n.push(w) } else n.push(g) } if (h) { var k = r(n[n.length - 1]); n.push(["Z"]), s(n[0], k) } } else n = o; return n.reduce((function (t, e) { return t + e.join(" ") + " " }), "") } }, { key: "drawLine", value: function (t, e, i, a) { var s = arguments.length > 4 && void 0 !== arguments[4] ? arguments[4] : "#a8a8a8", r = arguments.length > 5 && void 0 !== arguments[5] ? arguments[5] : 0, o = arguments.length > 6 && void 0 !== arguments[6] ? arguments[6] : null, n = arguments.length > 7 && void 0 !== arguments[7] ? arguments[7] : "butt"; return this.w.globals.dom.Paper.line().attr({ x1: t, y1: e, x2: i, y2: a, stroke: s, "stroke-dasharray": r, "stroke-width": o, "stroke-linecap": n }) } }, { key: "drawRect", value: function () { var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 0, e = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 0, i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : 0, a = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : 0, s = arguments.length > 4 && void 0 !== arguments[4] ? arguments[4] : 0, r = arguments.length > 5 && void 0 !== arguments[5] ? arguments[5] : "#fefefe", o = arguments.length > 6 && void 0 !== arguments[6] ? arguments[6] : 1, n = arguments.length > 7 && void 0 !== arguments[7] ? arguments[7] : null, l = arguments.length > 8 && void 0 !== arguments[8] ? arguments[8] : null, h = arguments.length > 9 && void 0 !== arguments[9] ? arguments[9] : 0, c = this.w.globals.dom.Paper.rect(); return c.attr({ x: t, y: e, width: i > 0 ? i : 0, height: a > 0 ? a : 0, rx: s, ry: s, opacity: o, "stroke-width": null !== n ? n : 0, stroke: null !== l ? l : "none", "stroke-dasharray": h }), c.node.setAttribute("fill", r), c } }, { key: "drawPolygon", value: function (t) { var e = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : "#e1e1e1", i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : 1, a = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : "none"; return this.w.globals.dom.Paper.polygon(t).attr({ fill: a, stroke: e, "stroke-width": i }) } }, { key: "drawCircle", value: function (t) { var e = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : null; t < 0 && (t = 0); var i = this.w.globals.dom.Paper.circle(2 * t); return null !== e && i.attr(e), i } }, { key: "drawPath", value: function (t) { var e = t.d, i = void 0 === e ? "" : e, a = t.stroke, s = void 0 === a ? "#a8a8a8" : a, r = t.strokeWidth, o = void 0 === r ? 1 : r, n = t.fill, l = t.fillOpacity, h = void 0 === l ? 1 : l, c = t.strokeOpacity, d = void 0 === c ? 1 : c, g = t.classes, u = t.strokeLinecap, p = void 0 === u ? null : u, f = t.strokeDashArray, x = void 0 === f ? 0 : f, b = this.w; return null === p && (p = b.config.stroke.lineCap), (i.indexOf("undefined") > -1 || i.indexOf("NaN") > -1) && (i = "M 0 ".concat(b.globals.gridHeight)), b.globals.dom.Paper.path(i).attr({ fill: n, "fill-opacity": h, stroke: s, "stroke-opacity": d, "stroke-linecap": p, "stroke-width": o, "stroke-dasharray": x, class: g }) } }, { key: "group", value: function () { var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : null, e = this.w.globals.dom.Paper.group(); return null !== t && e.attr(t), e } }, { key: "move", value: function (t, e) { var i = ["M", t, e].join(" "); return i } }, { key: "line", value: function (t, e) { var i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : null, a = null; return null === i ? a = [" L", t, e].join(" ") : "H" === i ? a = [" H", t].join(" ") : "V" === i && (a = [" V", e].join(" ")), a } }, { key: "curve", value: function (t, e, i, a, s, r) { var o = ["C", t, e, i, a, s, r].join(" "); return o } }, { key: "quadraticCurve", value: function (t, e, i, a) { return ["Q", t, e, i, a].join(" ") } }, { key: "arc", value: function (t, e, i, a, s, r, o) { var n = "A"; arguments.length > 7 && void 0 !== arguments[7] && arguments[7] && (n = "a"); var l = [n, t, e, i, a, s, r, o].join(" "); return l } }, { key: "renderPaths", value: function (t) { var i, a = t.j, s = t.realIndex, r = t.pathFrom, o = t.pathTo, n = t.stroke, l = t.strokeWidth, h = t.strokeLinecap, c = t.fill, d = t.animationDelay, g = t.initialSpeed, u = t.dataChangeSpeed, p = t.className, f = t.shouldClipToGrid, x = void 0 === f || f, m = t.bindEventsOnPaths, y = void 0 === m || m, w = t.drawShadow, k = void 0 === w || w, A = this.w, S = new v(this.ctx), C = new b(this.ctx), L = this.w.config.chart.animations.enabled, P = L && this.w.config.chart.animations.dynamicAnimation.enabled, M = !!(L && !A.globals.resized || P && A.globals.dataChanged && A.globals.shouldAnimate); M ? i = r : (i = o, A.globals.animationEnded = !0); var I = A.config.stroke.dashArray, T = 0; T = Array.isArray(I) ? I[s] : A.config.stroke.dashArray; var z = this.drawPath({ d: i, stroke: n, strokeWidth: l, fill: c, fillOpacity: 1, classes: p, strokeLinecap: h, strokeDashArray: T }); if (z.attr("index", s), x && z.attr({ "clip-path": "url(#gridRectMask".concat(A.globals.cuid, ")") }), "none" !== A.config.states.normal.filter.type) S.getDefaultFilter(z, s); else if (A.config.chart.dropShadow.enabled && k) { var X = A.config.chart.dropShadow; S.dropShadow(z, X, s) } y && (z.node.addEventListener("mouseenter", this.pathMouseEnter.bind(this, z)), z.node.addEventListener("mouseleave", this.pathMouseLeave.bind(this, z)), z.node.addEventListener("mousedown", this.pathMouseDown.bind(this, z))), z.attr({ pathTo: o, pathFrom: r }); var E = { el: z, j: a, realIndex: s, pathFrom: r, pathTo: o, fill: c, strokeWidth: l, delay: d }; return !L || A.globals.resized || A.globals.dataChanged ? !A.globals.resized && A.globals.dataChanged || C.showDelayedElements() : C.animatePathsGradually(e(e({}, E), {}, { speed: g })), A.globals.dataChanged && P && M && C.animatePathsGradually(e(e({}, E), {}, { speed: u })), z } }, { key: "drawPattern", value: function (t, e, i) { var a = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : "#a8a8a8", s = arguments.length > 4 && void 0 !== arguments[4] ? arguments[4] : 0; return this.w.globals.dom.Paper.pattern(e, i, (function (r) { "horizontalLines" === t ? r.line(0, 0, i, 0).stroke({ color: a, width: s + 1 }) : "verticalLines" === t ? r.line(0, 0, 0, e).stroke({ color: a, width: s + 1 }) : "slantedLines" === t ? r.line(0, 0, e, i).stroke({ color: a, width: s }) : "squares" === t ? r.rect(e, i).fill("none").stroke({ color: a, width: s }) : "circles" === t && r.circle(e).fill("none").stroke({ color: a, width: s }) })) } }, { key: "drawGradient", value: function (t, e, i, a, s) { var r, o = arguments.length > 5 && void 0 !== arguments[5] ? arguments[5] : null, n = arguments.length > 6 && void 0 !== arguments[6] ? arguments[6] : null, l = arguments.length > 7 && void 0 !== arguments[7] ? arguments[7] : null, h = arguments.length > 8 && void 0 !== arguments[8] ? arguments[8] : 0, c = this.w; e.length < 9 && 0 === e.indexOf("#") && (e = x.hexToRgba(e, a)), i.length < 9 && 0 === i.indexOf("#") && (i = x.hexToRgba(i, s)); var d = 0, g = 1, u = 1, p = null; null !== n && (d = void 0 !== n[0] ? n[0] / 100 : 0, g = void 0 !== n[1] ? n[1] / 100 : 1, u = void 0 !== n[2] ? n[2] / 100 : 1, p = void 0 !== n[3] ? n[3] / 100 : null); var f = !("donut" !== c.config.chart.type && "pie" !== c.config.chart.type && "polarArea" !== c.config.chart.type && "bubble" !== c.config.chart.type); if (r = null === l || 0 === l.length ? c.globals.dom.Paper.gradient(f ? "radial" : "linear", (function (t) { t.at(d, e, a), t.at(g, i, s), t.at(u, i, s), null !== p && t.at(p, e, a) })) : c.globals.dom.Paper.gradient(f ? "radial" : "linear", (function (t) { (Array.isArray(l[h]) ? l[h] : l).forEach((function (e) { t.at(e.offset / 100, e.color, e.opacity) })) })), f) { var b = c.globals.gridWidth / 2, v = c.globals.gridHeight / 2; "bubble" !== c.config.chart.type ? r.attr({ gradientUnits: "userSpaceOnUse", cx: b, cy: v, r: o }) : r.attr({ cx: .5, cy: .5, r: .8, fx: .2, fy: .2 }) } else "vertical" === t ? r.from(0, 0).to(0, 1) : "diagonal" === t ? r.from(0, 0).to(1, 1) : "horizontal" === t ? r.from(0, 1).to(1, 1) : "diagonal2" === t && r.from(1, 0).to(0, 1); return r } }, { key: "getTextBasedOnMaxWidth", value: function (t) { var e = t.text, i = t.maxWidth, a = t.fontSize, s = t.fontFamily, r = this.getTextRects(e, a, s), o = r.width / e.length, n = Math.floor(i / o); return i < r.width ? e.slice(0, n - 3) + "..." : e } }, { key: "drawText", value: function (t) { var i = this, a = t.x, s = t.y, r = t.text, o = t.textAnchor, n = t.fontSize, l = t.fontFamily, h = t.fontWeight, c = t.foreColor, d = t.opacity, g = t.maxWidth, u = t.cssClass, p = void 0 === u ? "" : u, f = t.isPlainText, x = void 0 === f || f, b = t.dominantBaseline, v = void 0 === b ? "auto" : b, m = this.w; void 0 === r && (r = ""); var y = r; o || (o = "start"), c && c.length || (c = m.config.chart.foreColor), l = l || m.config.chart.fontFamily, h = h || "regular"; var w, k = { maxWidth: g, fontSize: n = n || "11px", fontFamily: l }; return Array.isArray(r) ? w = m.globals.dom.Paper.text((function (t) { for (var a = 0; a < r.length; a++)y = r[a], g && (y = i.getTextBasedOnMaxWidth(e({ text: r[a] }, k))), 0 === a ? t.tspan(y) : t.tspan(y).newLine() })) : (g && (y = this.getTextBasedOnMaxWidth(e({ text: r }, k))), w = x ? m.globals.dom.Paper.plain(r) : m.globals.dom.Paper.text((function (t) { return t.tspan(y) }))), w.attr({ x: a, y: s, "text-anchor": o, "dominant-baseline": v, "font-size": n, "font-family": l, "font-weight": h, fill: c, class: "apexcharts-text " + p }), w.node.style.fontFamily = l, w.node.style.opacity = d, w } }, { key: "createGroupWithAttributes", value: function (t, e, i, a) { var s = this.group(); return i.forEach((function (t) { return s.add(t) })), s.attr({ class: a.class ? a.class : "", cy: e, cx: t }), s } }, { key: "drawPlus", value: function (t, e, i, a) { var s = i / 2, r = this.drawLine(t, e - s, t, e + s, a.pointStrokeColor, a.pointStrokeDashArray, a.pointStrokeWidth, a.pointStrokeLineCap), o = this.drawLine(t - s, e, t + s, e, a.pointStrokeColor, a.pointStrokeDashArray, a.pointStrokeWidth, a.pointStrokeLineCap); return this.createGroupWithAttributes(t, e, [r, o], a) } }, { key: "drawX", value: function (t, e, i, a) { var s = i / 2, r = this.drawLine(t - s, e - s, t + s, e + s, a.pointStrokeColor, a.pointStrokeDashArray, a.pointStrokeWidth, a.pointStrokeLineCap), o = this.drawLine(t - s, e + s, t + s, e - s, a.pointStrokeColor, a.pointStrokeDashArray, a.pointStrokeWidth, a.pointStrokeLineCap); return this.createGroupWithAttributes(t, e, [r, o], a) } }, { key: "drawMarker", value: function (t, e, i) { t = t || 0; var a = i.pSize || 0, s = null; if ("X" === (null == i ? void 0 : i.shape) || "x" === (null == i ? void 0 : i.shape)) s = this.drawX(t, e, a, i); else if ("plus" === (null == i ? void 0 : i.shape) || "+" === (null == i ? void 0 : i.shape)) s = this.drawPlus(t, e, a, i); else if ("square" === i.shape || "rect" === i.shape) { var r = void 0 === i.pRadius ? a / 2 : i.pRadius; null !== e && a || (a = 0, r = 0); var o = 1.2 * a + r, n = this.drawRect(o, o, o, o, r); n.attr({ x: t - o / 2, y: e - o / 2, cx: t, cy: e, class: i.class ? i.class : "", fill: i.pointFillColor, "fill-opacity": i.pointFillOpacity ? i.pointFillOpacity : 1, stroke: i.pointStrokeColor, "stroke-width": i.pointStrokeWidth ? i.pointStrokeWidth : 0, "stroke-opacity": i.pointStrokeOpacity ? i.pointStrokeOpacity : 1 }), s = n } else "circle" !== i.shape && i.shape || (x.isNumber(e) || (a = 0, e = 0), s = this.drawCircle(a, { cx: t, cy: e, class: i.class ? i.class : "", stroke: i.pointStrokeColor, fill: i.pointFillColor, "fill-opacity": i.pointFillOpacity ? i.pointFillOpacity : 1, "stroke-width": i.pointStrokeWidth ? i.pointStrokeWidth : 0, "stroke-opacity": i.pointStrokeOpacity ? i.pointStrokeOpacity : 1 })); return s } }, { key: "pathMouseEnter", value: function (t, e) { var i = this.w, a = new v(this.ctx), s = parseInt(t.node.getAttribute("index"), 10), r = parseInt(t.node.getAttribute("j"), 10); if ("function" == typeof i.config.chart.events.dataPointMouseEnter && i.config.chart.events.dataPointMouseEnter(e, this.ctx, { seriesIndex: s, dataPointIndex: r, w: i }), this.ctx.events.fireEvent("dataPointMouseEnter", [e, this.ctx, { seriesIndex: s, dataPointIndex: r, w: i }]), ("none" === i.config.states.active.filter.type || "true" !== t.node.getAttribute("selected")) && "none" !== i.config.states.hover.filter.type && !i.globals.isTouchDevice) { var o = i.config.states.hover.filter; a.applyFilter(t, s, o.type, o.value) } } }, { key: "pathMouseLeave", value: function (t, e) { var i = this.w, a = new v(this.ctx), s = parseInt(t.node.getAttribute("index"), 10), r = parseInt(t.node.getAttribute("j"), 10); "function" == typeof i.config.chart.events.dataPointMouseLeave && i.config.chart.events.dataPointMouseLeave(e, this.ctx, { seriesIndex: s, dataPointIndex: r, w: i }), this.ctx.events.fireEvent("dataPointMouseLeave", [e, this.ctx, { seriesIndex: s, dataPointIndex: r, w: i }]), "none" !== i.config.states.active.filter.type && "true" === t.node.getAttribute("selected") || "none" !== i.config.states.hover.filter.type && a.getDefaultFilter(t, s) } }, { key: "pathMouseDown", value: function (t, e) { var i = this.w, a = new v(this.ctx), s = parseInt(t.node.getAttribute("index"), 10), r = parseInt(t.node.getAttribute("j"), 10), o = "false"; if ("true" === t.node.getAttribute("selected")) { if (t.node.setAttribute("selected", "false"), i.globals.selectedDataPoints[s].indexOf(r) > -1) { var n = i.globals.selectedDataPoints[s].indexOf(r); i.globals.selectedDataPoints[s].splice(n, 1) } } else { if (!i.config.states.active.allowMultipleDataPointsSelection && i.globals.selectedDataPoints.length > 0) { i.globals.selectedDataPoints = []; var l = i.globals.dom.Paper.select(".apexcharts-series path").members, h = i.globals.dom.Paper.select(".apexcharts-series circle, .apexcharts-series rect").members, c = function (t) { Array.prototype.forEach.call(t, (function (t) { t.node.setAttribute("selected", "false"), a.getDefaultFilter(t, s) })) }; c(l), c(h) } t.node.setAttribute("selected", "true"), o = "true", void 0 === i.globals.selectedDataPoints[s] && (i.globals.selectedDataPoints[s] = []), i.globals.selectedDataPoints[s].push(r) } if ("true" === o) { var d = i.config.states.active.filter; if ("none" !== d) a.applyFilter(t, s, d.type, d.value); else if ("none" !== i.config.states.hover.filter && !i.globals.isTouchDevice) { var g = i.config.states.hover.filter; a.applyFilter(t, s, g.type, g.value) } } else if ("none" !== i.config.states.active.filter.type) if ("none" === i.config.states.hover.filter.type || i.globals.isTouchDevice) a.getDefaultFilter(t, s); else { g = i.config.states.hover.filter; a.applyFilter(t, s, g.type, g.value) } "function" == typeof i.config.chart.events.dataPointSelection && i.config.chart.events.dataPointSelection(e, this.ctx, { selectedDataPoints: i.globals.selectedDataPoints, seriesIndex: s, dataPointIndex: r, w: i }), e && this.ctx.events.fireEvent("dataPointSelection", [e, this.ctx, { selectedDataPoints: i.globals.selectedDataPoints, seriesIndex: s, dataPointIndex: r, w: i }]) } }, { key: "rotateAroundCenter", value: function (t) { var e = {}; return t && "function" == typeof t.getBBox && (e = t.getBBox()), { x: e.x + e.width / 2, y: e.y + e.height / 2 } } }, { key: "getTextRects", value: function (t, e, i, a) { var s = !(arguments.length > 4 && void 0 !== arguments[4]) || arguments[4], r = this.w, o = this.drawText({ x: -200, y: -200, text: t, textAnchor: "start", fontSize: e, fontFamily: i, foreColor: "#fff", opacity: 0 }); a && o.attr("transform", a), r.globals.dom.Paper.add(o); var n = o.bbox(); return s || (n = o.node.getBoundingClientRect()), o.remove(), { width: n.width, height: n.height } } }, { key: "placeTextWithEllipsis", value: function (t, e, i) { if ("function" == typeof t.getComputedTextLength && (t.textContent = e, e.length > 0 && t.getComputedTextLength() >= i / 1.1)) { for (var a = e.length - 3; a > 0; a -= 3)if (t.getSubStringLength(0, a) <= i / 1.1) return void (t.textContent = e.substring(0, a) + "..."); t.textContent = "." } } }], [{ key: "setAttrs", value: function (t, e) { for (var i in e) e.hasOwnProperty(i) && t.setAttribute(i, e[i]) } }]), t }(), y = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w } return r(t, [{ key: "getStackedSeriesTotals", value: function () { var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : [], e = this.w, i = []; if (0 === e.globals.series.length) return i; for (var a = 0; a < e.globals.series[e.globals.maxValsInArrayIndex].length; a++) { for (var s = 0, r = 0; r < e.globals.series.length; r++)void 0 !== e.globals.series[r][a] && -1 === t.indexOf(r) && (s += e.globals.series[r][a]); i.push(s) } return i } }, { key: "getSeriesTotalByIndex", value: function () { var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : null; return null === t ? this.w.config.series.reduce((function (t, e) { return t + e }), 0) : this.w.globals.series[t].reduce((function (t, e) { return t + e }), 0) } }, { key: "getStackedSeriesTotalsByGroups", value: function () { var t = this, e = this.w, i = []; return e.globals.seriesGroups.forEach((function (a) { var s = []; e.config.series.forEach((function (t, i) { a.indexOf(e.globals.seriesNames[i]) > -1 && s.push(i) })); var r = e.globals.series.map((function (t, e) { return -1 === s.indexOf(e) ? e : -1 })).filter((function (t) { return -1 !== t })); i.push(t.getStackedSeriesTotals(r)) })), i } }, { key: "setSeriesYAxisMappings", value: function () { var t = this.w.globals, e = this.w.config, i = [], a = [], s = [], r = t.series.length > e.yaxis.length || e.yaxis.some((function (t) { return Array.isArray(t.seriesName) })); e.series.forEach((function (t, e) { s.push(e), a.push(null) })), e.yaxis.forEach((function (t, e) { i[e] = [] })); var o = []; e.yaxis.forEach((function (t, a) { var n = !1; if (t.seriesName) { var l = []; Array.isArray(t.seriesName) ? l = t.seriesName : l.push(t.seriesName), l.forEach((function (t) { e.series.forEach((function (e, o) { if (e.name === t) { var l = o; a === o || r ? !r || s.indexOf(o) > -1 ? i[a].push([a, o]) : console.warn("Series '" + e.name + "' referenced more than once in what looks like the new style. That is, when using either seriesName: [], or when there are more series than yaxes.") : (i[o].push([o, a]), l = a), n = !0, -1 !== (l = s.indexOf(l)) && s.splice(l, 1) } })) })) } n || o.push(a) })), i = i.map((function (t, e) { var i = []; return t.forEach((function (t) { a[t[1]] = t[0], i.push(t[1]) })), i })); for (var n = e.yaxis.length - 1, l = 0; l < o.length && (n = o[l], i[n] = [], s); l++) { var h = s[0]; s.shift(), i[n].push(h), a[h] = n } s.forEach((function (t) { i[n].push(t), a[t] = n })), t.seriesYAxisMap = i.map((function (t) { return t })), t.seriesYAxisReverseMap = a.map((function (t) { return t })), t.seriesYAxisMap.forEach((function (t, i) { t.forEach((function (t) { e.series[t] && void 0 === e.series[t].group && (e.series[t].group = "apexcharts-axis-".concat(i.toString())) })) })) } }, { key: "isSeriesNull", value: function () { var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : null; return 0 === (null === t ? this.w.config.series.filter((function (t) { return null !== t })) : this.w.config.series[t].data.filter((function (t) { return null !== t }))).length } }, { key: "seriesHaveSameValues", value: function (t) { return this.w.globals.series[t].every((function (t, e, i) { return t === i[0] })) } }, { key: "getCategoryLabels", value: function (t) { var e = this.w, i = t.slice(); return e.config.xaxis.convertedCatToNumeric && (i = t.map((function (t, i) { return e.config.xaxis.labels.formatter(t - e.globals.minX + 1) }))), i } }, { key: "getLargestSeries", value: function () { var t = this.w; t.globals.maxValsInArrayIndex = t.globals.series.map((function (t) { return t.length })).indexOf(Math.max.apply(Math, t.globals.series.map((function (t) { return t.length })))) } }, { key: "getLargestMarkerSize", value: function () { var t = this.w, e = 0; return t.globals.markers.size.forEach((function (t) { e = Math.max(e, t) })), t.config.markers.discrete && t.config.markers.discrete.length && t.config.markers.discrete.forEach((function (t) { e = Math.max(e, t.size) })), e > 0 && (e += t.config.markers.hover.sizeOffset + 1), t.globals.markers.largestSize = e, e } }, { key: "getSeriesTotals", value: function () { var t = this.w; t.globals.seriesTotals = t.globals.series.map((function (t, e) { var i = 0; if (Array.isArray(t)) for (var a = 0; a < t.length; a++)i += t[a]; else i += t; return i })) } }, { key: "getSeriesTotalsXRange", value: function (t, e) { var i = this.w; return i.globals.series.map((function (a, s) { for (var r = 0, o = 0; o < a.length; o++)i.globals.seriesX[s][o] > t && i.globals.seriesX[s][o] < e && (r += a[o]); return r })) } }, { key: "getPercentSeries", value: function () { var t = this.w; t.globals.seriesPercent = t.globals.series.map((function (e, i) { var a = []; if (Array.isArray(e)) for (var s = 0; s < e.length; s++) { var r = t.globals.stackedSeriesTotals[s], o = 0; r && (o = 100 * e[s] / r), a.push(o) } else { var n = 100 * e / t.globals.seriesTotals.reduce((function (t, e) { return t + e }), 0); a.push(n) } return a })) } }, { key: "getCalculatedRatios", value: function () { var t, e, i, a = this, s = this.w, r = s.globals, o = [], n = 0, l = [], h = .1, c = 0; if (r.yRange = [], r.isMultipleYAxis) for (var d = 0; d < r.minYArr.length; d++)r.yRange.push(Math.abs(r.minYArr[d] - r.maxYArr[d])), l.push(0); else r.yRange.push(Math.abs(r.minY - r.maxY)); r.xRange = Math.abs(r.maxX - r.minX), r.zRange = Math.abs(r.maxZ - r.minZ); for (var g = 0; g < r.yRange.length; g++)o.push(r.yRange[g] / r.gridHeight); if (e = r.xRange / r.gridWidth, t = r.yRange / r.gridWidth, i = r.xRange / r.gridHeight, (n = r.zRange / r.gridHeight * 16) || (n = 1), r.minY !== Number.MIN_VALUE && 0 !== Math.abs(r.minY) && (r.hasNegs = !0), s.globals.seriesYAxisReverseMap.length > 0) { var u = function (t, e) { var i = s.config.yaxis[s.globals.seriesYAxisReverseMap[e]], r = t < 0 ? -1 : 1; return t = Math.abs(t), i.logarithmic && (t = a.getBaseLog(i.logBase, t)), -r * t / o[e] }; if (r.isMultipleYAxis) { l = []; for (var p = 0; p < o.length; p++)l.push(u(r.minYArr[p], p)) } else (l = []).push(u(r.minY, 0)), r.minY !== Number.MIN_VALUE && 0 !== Math.abs(r.minY) && (h = -r.minY / t, c = r.minX / e) } else (l = []).push(0), h = 0, c = 0; return { yRatio: o, invertedYRatio: t, zRatio: n, xRatio: e, invertedXRatio: i, baseLineInvertedY: h, baseLineY: l, baseLineX: c } } }, { key: "getLogSeries", value: function (t) { var e = this, i = this.w; return i.globals.seriesLog = t.map((function (t, a) { var s = i.globals.seriesYAxisReverseMap[a]; return i.config.yaxis[s] && i.config.yaxis[s].logarithmic ? t.map((function (t) { return null === t ? null : e.getLogVal(i.config.yaxis[s].logBase, t, a) })) : t })), i.globals.invalidLogScale ? t : i.globals.seriesLog } }, { key: "getBaseLog", value: function (t, e) { return Math.log(e) / Math.log(t) } }, { key: "getLogVal", value: function (t, e, i) { if (e <= 0) return 0; var a = this.w, s = 0 === a.globals.minYArr[i] ? -1 : this.getBaseLog(t, a.globals.minYArr[i]), r = (0 === a.globals.maxYArr[i] ? 0 : this.getBaseLog(t, a.globals.maxYArr[i])) - s; return e < 1 ? e / r : (this.getBaseLog(t, e) - s) / r } }, { key: "getLogYRatios", value: function (t) { var e = this, i = this.w, a = this.w.globals; return a.yLogRatio = t.slice(), a.logYRange = a.yRange.map((function (t, s) { var r = i.globals.seriesYAxisReverseMap[s]; if (i.config.yaxis[r] && e.w.config.yaxis[r].logarithmic) { var o, n = -Number.MAX_VALUE, l = Number.MIN_VALUE; return a.seriesLog.forEach((function (t, e) { t.forEach((function (t) { i.config.yaxis[e] && i.config.yaxis[e].logarithmic && (n = Math.max(t, n), l = Math.min(t, l)) })) })), o = Math.pow(a.yRange[s], Math.abs(l - n) / a.yRange[s]), a.yLogRatio[s] = o / a.gridHeight, o } })), a.invalidLogScale ? t.slice() : a.yLogRatio } }, { key: "drawSeriesByGroup", value: function (t, e, i, a) { var s = this.w, r = []; return t.series.length > 0 && e.forEach((function (e) { var o = [], n = []; t.i.forEach((function (i, a) { s.config.series[i].group === e && (o.push(t.series[a]), n.push(i)) })), o.length > 0 && r.push(a.draw(o, i, n)) })), r } }], [{ key: "checkComboSeries", value: function (t, e) { var i = !1, a = 0, s = 0; return void 0 === e && (e = "line"), t.length && void 0 !== t[0].type && t.forEach((function (t) { "bar" !== t.type && "column" !== t.type && "candlestick" !== t.type && "boxPlot" !== t.type || a++, void 0 !== t.type && t.type !== e && s++ })), s > 0 && (i = !0), { comboBarCount: a, comboCharts: i } } }, { key: "extendArrayProps", value: function (t, e, i) { var a, s, r, o, n, l; (null !== (a = e) && void 0 !== a && a.yaxis && (e = t.extendYAxis(e, i)), null !== (s = e) && void 0 !== s && s.annotations) && (e.annotations.yaxis && (e = t.extendYAxisAnnotations(e)), null !== (r = e) && void 0 !== r && null !== (o = r.annotations) && void 0 !== o && o.xaxis && (e = t.extendXAxisAnnotations(e)), null !== (n = e) && void 0 !== n && null !== (l = n.annotations) && void 0 !== l && l.points && (e = t.extendPointAnnotations(e))); return e } }]), t }(), w = function () { function t(e) { a(this, t), this.w = e.w, this.annoCtx = e } return r(t, [{ key: "setOrientations", value: function (t) { var e = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : null, i = this.w; if ("vertical" === t.label.orientation) { var a = null !== e ? e : 0, s = i.globals.dom.baseEl.querySelector(".apexcharts-xaxis-annotations .apexcharts-xaxis-annotation-label[rel='".concat(a, "']")); if (null !== s) { var r = s.getBoundingClientRect(); s.setAttribute("x", parseFloat(s.getAttribute("x")) - r.height + 4), "top" === t.label.position ? s.setAttribute("y", parseFloat(s.getAttribute("y")) + r.width) : s.setAttribute("y", parseFloat(s.getAttribute("y")) - r.width); var o = this.annoCtx.graphics.rotateAroundCenter(s), n = o.x, l = o.y; s.setAttribute("transform", "rotate(-90 ".concat(n, " ").concat(l, ")")) } } } }, { key: "addBackgroundToAnno", value: function (t, e) { var i = this.w; if (!t || void 0 === e.label.text || void 0 !== e.label.text && !String(e.label.text).trim()) return null; var a = i.globals.dom.baseEl.querySelector(".apexcharts-grid").getBoundingClientRect(), s = t.getBoundingClientRect(), r = e.label.style.padding.left, o = e.label.style.padding.right, n = e.label.style.padding.top, l = e.label.style.padding.bottom; "vertical" === e.label.orientation && (n = e.label.style.padding.left, l = e.label.style.padding.right, r = e.label.style.padding.top, o = e.label.style.padding.bottom); var h = s.left - a.left - r, c = s.top - a.top - n, d = this.annoCtx.graphics.drawRect(h - i.globals.barPadForNumericAxis, c, s.width + r + o, s.height + n + l, e.label.borderRadius, e.label.style.background, 1, e.label.borderWidth, e.label.borderColor, 0); return e.id && d.node.classList.add(e.id), d } }, { key: "annotationsBackground", value: function () { var t = this, e = this.w, i = function (i, a, s) { var r = e.globals.dom.baseEl.querySelector(".apexcharts-".concat(s, "-annotations .apexcharts-").concat(s, "-annotation-label[rel='").concat(a, "']")); if (r) { var o = r.parentNode, n = t.addBackgroundToAnno(r, i); n && (o.insertBefore(n.node, r), i.label.mouseEnter && n.node.addEventListener("mouseenter", i.label.mouseEnter.bind(t, i)), i.label.mouseLeave && n.node.addEventListener("mouseleave", i.label.mouseLeave.bind(t, i)), i.label.click && n.node.addEventListener("click", i.label.click.bind(t, i))) } }; e.config.annotations.xaxis.map((function (t, e) { i(t, e, "xaxis") })), e.config.annotations.yaxis.map((function (t, e) { i(t, e, "yaxis") })), e.config.annotations.points.map((function (t, e) { i(t, e, "point") })) } }, { key: "getY1Y2", value: function (t, e) { var i, a = "y1" === t ? e.y : e.y2, s = !1, r = this.w; if (this.annoCtx.invertAxis) { var o = r.globals.labels; r.config.xaxis.convertedCatToNumeric && (o = r.globals.categoryLabels); var n = o.indexOf(a), l = r.globals.dom.baseEl.querySelector(".apexcharts-yaxis-texts-g text:nth-child(" + (n + 1) + ")"); i = l ? parseFloat(l.getAttribute("y")) : (r.globals.gridHeight / o.length - 1) * (n + 1) - r.globals.barHeight, void 0 !== e.seriesIndex && r.globals.barHeight && (i = i - r.globals.barHeight / 2 * (r.globals.series.length - 1) + r.globals.barHeight * e.seriesIndex) } else { var h, c = r.globals.seriesYAxisMap[e.yAxisIndex][0]; if (r.config.yaxis[e.yAxisIndex].logarithmic) h = (a = new y(this.annoCtx.ctx).getLogVal(r.config.yaxis[e.yAxisIndex].logBase, a, c)) / r.globals.yLogRatio[c]; else h = (a - r.globals.minYArr[c]) / (r.globals.yRange[c] / r.globals.gridHeight); h > r.globals.gridHeight ? (h = r.globals.gridHeight, s = !0) : h < 0 && (h = 0, s = !0), i = r.globals.gridHeight - h, !e.marker || void 0 !== e.y && null !== e.y || (i = 0), r.config.yaxis[e.yAxisIndex] && r.config.yaxis[e.yAxisIndex].reversed && (i = h) } return "string" == typeof a && a.indexOf("px") > -1 && (i = parseFloat(a)), { yP: i, clipped: s } } }, { key: "getX1X2", value: function (t, e) { var i, a = "x1" === t ? e.x : e.x2, s = this.w, r = this.annoCtx.invertAxis ? s.globals.minY : s.globals.minX, o = this.annoCtx.invertAxis ? s.globals.maxY : s.globals.maxX, n = this.annoCtx.invertAxis ? s.globals.yRange[0] : s.globals.xRange, l = !1; return i = this.annoCtx.inversedReversedAxis ? (o - a) / (n / s.globals.gridWidth) : (a - r) / (n / s.globals.gridWidth), "category" !== s.config.xaxis.type && !s.config.xaxis.convertedCatToNumeric || this.annoCtx.invertAxis || s.globals.dataFormatXNumeric || s.config.chart.sparkline.enabled || (i = this.getStringX(a)), "string" == typeof a && a.indexOf("px") > -1 && (i = parseFloat(a)), null == a && e.marker && (i = s.globals.gridWidth), void 0 !== e.seriesIndex && s.globals.barWidth && !this.annoCtx.invertAxis && (i = i - s.globals.barWidth / 2 * (s.globals.series.length - 1) + s.globals.barWidth * e.seriesIndex), i > s.globals.gridWidth ? (i = s.globals.gridWidth, l = !0) : i < 0 && (i = 0, l = !0), { x: i, clipped: l } } }, { key: "getStringX", value: function (t) { var e = this.w, i = t; e.config.xaxis.convertedCatToNumeric && e.globals.categoryLabels.length && (t = e.globals.categoryLabels.indexOf(t) + 1); var a = e.globals.labels.indexOf(t), s = e.globals.dom.baseEl.querySelector(".apexcharts-xaxis-texts-g text:nth-child(" + (a + 1) + ")"); return s && (i = parseFloat(s.getAttribute("x"))), i } }]), t }(), k = function () { function t(e) { a(this, t), this.w = e.w, this.annoCtx = e, this.invertAxis = this.annoCtx.invertAxis, this.helpers = new w(this.annoCtx) } return r(t, [{ key: "addXaxisAnnotation", value: function (t, e, i) { var a, s = this.w, r = this.helpers.getX1X2("x1", t), o = r.x, n = r.clipped, l = !0, h = t.label.text, c = t.strokeDashArray; if (x.isNumber(o)) { if (null === t.x2 || void 0 === t.x2) { if (!n) { var d = this.annoCtx.graphics.drawLine(o + t.offsetX, 0 + t.offsetY, o + t.offsetX, s.globals.gridHeight + t.offsetY, t.borderColor, c, t.borderWidth); e.appendChild(d.node), t.id && d.node.classList.add(t.id) } } else { var g = this.helpers.getX1X2("x2", t); if (a = g.x, l = g.clipped, !n || !l) { if (a < o) { var u = o; o = a, a = u } var p = this.annoCtx.graphics.drawRect(o + t.offsetX, 0 + t.offsetY, a - o, s.globals.gridHeight + t.offsetY, 0, t.fillColor, t.opacity, 1, t.borderColor, c); p.node.classList.add("apexcharts-annotation-rect"), p.attr("clip-path", "url(#gridRectMask".concat(s.globals.cuid, ")")), e.appendChild(p.node), t.id && p.node.classList.add(t.id) } } if (!n || !l) { var f = this.annoCtx.graphics.getTextRects(h, parseFloat(t.label.style.fontSize)), b = "top" === t.label.position ? 4 : "center" === t.label.position ? s.globals.gridHeight / 2 + ("vertical" === t.label.orientation ? f.width / 2 : 0) : s.globals.gridHeight, v = this.annoCtx.graphics.drawText({ x: o + t.label.offsetX, y: b + t.label.offsetY - ("vertical" === t.label.orientation ? "top" === t.label.position ? f.width / 2 - 12 : -f.width / 2 : 0), text: h, textAnchor: t.label.textAnchor, fontSize: t.label.style.fontSize, fontFamily: t.label.style.fontFamily, fontWeight: t.label.style.fontWeight, foreColor: t.label.style.color, cssClass: "apexcharts-xaxis-annotation-label ".concat(t.label.style.cssClass, " ").concat(t.id ? t.id : "") }); v.attr({ rel: i }), e.appendChild(v.node), this.annoCtx.helpers.setOrientations(t, i) } } } }, { key: "drawXAxisAnnotations", value: function () { var t = this, e = this.w, i = this.annoCtx.graphics.group({ class: "apexcharts-xaxis-annotations" }); return e.config.annotations.xaxis.map((function (e, a) { t.addXaxisAnnotation(e, i.node, a) })), i } }]), t }(), A = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w, this.months31 = [1, 3, 5, 7, 8, 10, 12], this.months30 = [2, 4, 6, 9, 11], this.daysCntOfYear = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334] } return r(t, [{ key: "isValidDate", value: function (t) { return "number" != typeof t && !isNaN(this.parseDate(t)) } }, { key: "getTimeStamp", value: function (t) { return Date.parse(t) ? this.w.config.xaxis.labels.datetimeUTC ? new Date(new Date(t).toISOString().substr(0, 25)).getTime() : new Date(t).getTime() : t } }, { key: "getDate", value: function (t) { return this.w.config.xaxis.labels.datetimeUTC ? new Date(new Date(t).toUTCString()) : new Date(t) } }, { key: "parseDate", value: function (t) { var e = Date.parse(t); if (!isNaN(e)) return this.getTimeStamp(t); var i = Date.parse(t.replace(/-/g, "/").replace(/[a-z]+/gi, " ")); return i = this.getTimeStamp(i) } }, { key: "parseDateWithTimezone", value: function (t) { return Date.parse(t.replace(/-/g, "/").replace(/[a-z]+/gi, " ")) } }, { key: "formatDate", value: function (t, e) { var i = this.w.globals.locale, a = this.w.config.xaxis.labels.datetimeUTC, s = ["\0"].concat(u(i.months)), r = ["\x01"].concat(u(i.shortMonths)), o = ["\x02"].concat(u(i.days)), n = ["\x03"].concat(u(i.shortDays)); function l(t, e) { var i = t + ""; for (e = e || 2; i.length < e;)i = "0" + i; return i } var h = a ? t.getUTCFullYear() : t.getFullYear(); e = (e = (e = e.replace(/(^|[^\\])yyyy+/g, "$1" + h)).replace(/(^|[^\\])yy/g, "$1" + h.toString().substr(2, 2))).replace(/(^|[^\\])y/g, "$1" + h); var c = (a ? t.getUTCMonth() : t.getMonth()) + 1; e = (e = (e = (e = e.replace(/(^|[^\\])MMMM+/g, "$1" + s[0])).replace(/(^|[^\\])MMM/g, "$1" + r[0])).replace(/(^|[^\\])MM/g, "$1" + l(c))).replace(/(^|[^\\])M/g, "$1" + c); var d = a ? t.getUTCDate() : t.getDate(); e = (e = (e = (e = e.replace(/(^|[^\\])dddd+/g, "$1" + o[0])).replace(/(^|[^\\])ddd/g, "$1" + n[0])).replace(/(^|[^\\])dd/g, "$1" + l(d))).replace(/(^|[^\\])d/g, "$1" + d); var g = a ? t.getUTCHours() : t.getHours(), p = g > 12 ? g - 12 : 0 === g ? 12 : g; e = (e = (e = (e = e.replace(/(^|[^\\])HH+/g, "$1" + l(g))).replace(/(^|[^\\])H/g, "$1" + g)).replace(/(^|[^\\])hh+/g, "$1" + l(p))).replace(/(^|[^\\])h/g, "$1" + p); var f = a ? t.getUTCMinutes() : t.getMinutes(); e = (e = e.replace(/(^|[^\\])mm+/g, "$1" + l(f))).replace(/(^|[^\\])m/g, "$1" + f); var x = a ? t.getUTCSeconds() : t.getSeconds(); e = (e = e.replace(/(^|[^\\])ss+/g, "$1" + l(x))).replace(/(^|[^\\])s/g, "$1" + x); var b = a ? t.getUTCMilliseconds() : t.getMilliseconds(); e = e.replace(/(^|[^\\])fff+/g, "$1" + l(b, 3)), b = Math.round(b / 10), e = e.replace(/(^|[^\\])ff/g, "$1" + l(b)), b = Math.round(b / 10); var v = g < 12 ? "AM" : "PM"; e = (e = (e = e.replace(/(^|[^\\])f/g, "$1" + b)).replace(/(^|[^\\])TT+/g, "$1" + v)).replace(/(^|[^\\])T/g, "$1" + v.charAt(0)); var m = v.toLowerCase(); e = (e = e.replace(/(^|[^\\])tt+/g, "$1" + m)).replace(/(^|[^\\])t/g, "$1" + m.charAt(0)); var y = -t.getTimezoneOffset(), w = a || !y ? "Z" : y > 0 ? "+" : "-"; if (!a) { var k = (y = Math.abs(y)) % 60; w += l(Math.floor(y / 60)) + ":" + l(k) } e = e.replace(/(^|[^\\])K/g, "$1" + w); var A = (a ? t.getUTCDay() : t.getDay()) + 1; return e = (e = (e = (e = (e = e.replace(new RegExp(o[0], "g"), o[A])).replace(new RegExp(n[0], "g"), n[A])).replace(new RegExp(s[0], "g"), s[c])).replace(new RegExp(r[0], "g"), r[c])).replace(/\\(.)/g, "$1") } }, { key: "getTimeUnitsfromTimestamp", value: function (t, e, i) { var a = this.w; void 0 !== a.config.xaxis.min && (t = a.config.xaxis.min), void 0 !== a.config.xaxis.max && (e = a.config.xaxis.max); var s = this.getDate(t), r = this.getDate(e), o = this.formatDate(s, "yyyy MM dd HH mm ss fff").split(" "), n = this.formatDate(r, "yyyy MM dd HH mm ss fff").split(" "); return { minMillisecond: parseInt(o[6], 10), maxMillisecond: parseInt(n[6], 10), minSecond: parseInt(o[5], 10), maxSecond: parseInt(n[5], 10), minMinute: parseInt(o[4], 10), maxMinute: parseInt(n[4], 10), minHour: parseInt(o[3], 10), maxHour: parseInt(n[3], 10), minDate: parseInt(o[2], 10), maxDate: parseInt(n[2], 10), minMonth: parseInt(o[1], 10) - 1, maxMonth: parseInt(n[1], 10) - 1, minYear: parseInt(o[0], 10), maxYear: parseInt(n[0], 10) } } }, { key: "isLeapYear", value: function (t) { return t % 4 == 0 && t % 100 != 0 || t % 400 == 0 } }, { key: "calculcateLastDaysOfMonth", value: function (t, e, i) { return this.determineDaysOfMonths(t, e) - i } }, { key: "determineDaysOfYear", value: function (t) { var e = 365; return this.isLeapYear(t) && (e = 366), e } }, { key: "determineRemainingDaysOfYear", value: function (t, e, i) { var a = this.daysCntOfYear[e] + i; return e > 1 && this.isLeapYear() && a++, a } }, { key: "determineDaysOfMonths", value: function (t, e) { var i = 30; switch (t = x.monthMod(t), !0) { case this.months30.indexOf(t) > -1: 2 === t && (i = this.isLeapYear(e) ? 29 : 28); break; case this.months31.indexOf(t) > -1: default: i = 31 }return i } }]), t }(), S = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w, this.tooltipKeyFormat = "dd MMM" } return r(t, [{ key: "xLabelFormat", value: function (t, e, i, a) { var s = this.w; if ("datetime" === s.config.xaxis.type && void 0 === s.config.xaxis.labels.formatter && void 0 === s.config.tooltip.x.formatter) { var r = new A(this.ctx); return r.formatDate(r.getDate(e), s.config.tooltip.x.format) } return t(e, i, a) } }, { key: "defaultGeneralFormatter", value: function (t) { return Array.isArray(t) ? t.map((function (t) { return t })) : t } }, { key: "defaultYFormatter", value: function (t, e, i) { var a = this.w; if (x.isNumber(t)) if (0 !== a.globals.yValueDecimal) t = t.toFixed(void 0 !== e.decimalsInFloat ? e.decimalsInFloat : a.globals.yValueDecimal); else { var s = t.toFixed(0); t = t == s ? s : t.toFixed(1) } return t } }, { key: "setLabelFormatters", value: function () { var t = this, e = this.w; return e.globals.xaxisTooltipFormatter = function (e) { return t.defaultGeneralFormatter(e) }, e.globals.ttKeyFormatter = function (e) { return t.defaultGeneralFormatter(e) }, e.globals.ttZFormatter = function (t) { return t }, e.globals.legendFormatter = function (e) { return t.defaultGeneralFormatter(e) }, void 0 !== e.config.xaxis.labels.formatter ? e.globals.xLabelFormatter = e.config.xaxis.labels.formatter : e.globals.xLabelFormatter = function (t) { if (x.isNumber(t)) { if (!e.config.xaxis.convertedCatToNumeric && "numeric" === e.config.xaxis.type) { if (x.isNumber(e.config.xaxis.decimalsInFloat)) return t.toFixed(e.config.xaxis.decimalsInFloat); var i = e.globals.maxX - e.globals.minX; return i > 0 && i < 100 ? t.toFixed(1) : t.toFixed(0) } if (e.globals.isBarHorizontal) if (e.globals.maxY - e.globals.minYArr < 4) return t.toFixed(1); return t.toFixed(0) } return t }, "function" == typeof e.config.tooltip.x.formatter ? e.globals.ttKeyFormatter = e.config.tooltip.x.formatter : e.globals.ttKeyFormatter = e.globals.xLabelFormatter, "function" == typeof e.config.xaxis.tooltip.formatter && (e.globals.xaxisTooltipFormatter = e.config.xaxis.tooltip.formatter), (Array.isArray(e.config.tooltip.y) || void 0 !== e.config.tooltip.y.formatter) && (e.globals.ttVal = e.config.tooltip.y), void 0 !== e.config.tooltip.z.formatter && (e.globals.ttZFormatter = e.config.tooltip.z.formatter), void 0 !== e.config.legend.formatter && (e.globals.legendFormatter = e.config.legend.formatter), e.config.yaxis.forEach((function (i, a) { void 0 !== i.labels.formatter ? e.globals.yLabelFormatters[a] = i.labels.formatter : e.globals.yLabelFormatters[a] = function (s) { return e.globals.xyCharts ? Array.isArray(s) ? s.map((function (e) { return t.defaultYFormatter(e, i, a) })) : t.defaultYFormatter(s, i, a) : s } })), e.globals } }, { key: "heatmapLabelFormatters", value: function () { var t = this.w; if ("heatmap" === t.config.chart.type) { t.globals.yAxisScale[0].result = t.globals.seriesNames.slice(); var e = t.globals.seriesNames.reduce((function (t, e) { return t.length > e.length ? t : e }), 0); t.globals.yAxisScale[0].niceMax = e, t.globals.yAxisScale[0].niceMin = e } } }]), t }(), C = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w } return r(t, [{ key: "getLabel", value: function (t, e, i, a) { var s = arguments.length > 4 && void 0 !== arguments[4] ? arguments[4] : [], r = arguments.length > 5 && void 0 !== arguments[5] ? arguments[5] : "12px", o = !(arguments.length > 6 && void 0 !== arguments[6]) || arguments[6], n = this.w, l = void 0 === t[a] ? "" : t[a], h = l, c = n.globals.xLabelFormatter, d = n.config.xaxis.labels.formatter, g = !1, u = new S(this.ctx), p = l; o && (h = u.xLabelFormat(c, l, p, { i: a, dateFormatter: new A(this.ctx).formatDate, w: n }), void 0 !== d && (h = d(l, t[a], { i: a, dateFormatter: new A(this.ctx).formatDate, w: n }))); var f, x; e.length > 0 ? (f = e[a].unit, x = null, e.forEach((function (t) { "month" === t.unit ? x = "year" : "day" === t.unit ? x = "month" : "hour" === t.unit ? x = "day" : "minute" === t.unit && (x = "hour") })), g = x === f, i = e[a].position, h = e[a].value) : "datetime" === n.config.xaxis.type && void 0 === d && (h = ""), void 0 === h && (h = ""), h = Array.isArray(h) ? h : h.toString(); var b = new m(this.ctx), v = {}; v = n.globals.rotateXLabels && o ? b.getTextRects(h, parseInt(r, 10), null, "rotate(".concat(n.config.xaxis.labels.rotate, " 0 0)"), !1) : b.getTextRects(h, parseInt(r, 10)); var y = !n.config.xaxis.labels.showDuplicates && this.ctx.timeScale; return !Array.isArray(h) && ("NaN" === String(h) || s.indexOf(h) >= 0 && y) && (h = ""), { x: i, text: h, textRect: v, isBold: g } } }, { key: "checkLabelBasedOnTickamount", value: function (t, e, i) { var a = this.w, s = a.config.xaxis.tickAmount; return "dataPoints" === s && (s = Math.round(a.globals.gridWidth / 120)), s > i || t % Math.round(i / (s + 1)) == 0 || (e.text = ""), e } }, { key: "checkForOverflowingLabels", value: function (t, e, i, a, s) { var r = this.w; if (0 === t && r.globals.skipFirstTimelinelabel && (e.text = ""), t === i - 1 && r.globals.skipLastTimelinelabel && (e.text = ""), r.config.xaxis.labels.hideOverlappingLabels && a.length > 0) { var o = s[s.length - 1]; e.x < o.textRect.width / (r.globals.rotateXLabels ? Math.abs(r.config.xaxis.labels.rotate) / 12 : 1.01) + o.x && (e.text = "") } return e } }, { key: "checkForReversedLabels", value: function (t, e) { var i = this.w; return i.config.yaxis[t] && i.config.yaxis[t].reversed && e.reverse(), e } }, { key: "yAxisAllSeriesCollapsed", value: function (t) { var e = this.w.globals; return !e.seriesYAxisMap[t].some((function (t) { return -1 === e.collapsedSeriesIndices.indexOf(t) })) } }, { key: "translateYAxisIndex", value: function (t) { var e = this.w, i = e.globals, a = e.config.yaxis; return i.series.length > a.length || a.some((function (t) { return Array.isArray(t.seriesName) })) ? t : i.seriesYAxisReverseMap[t] } }, { key: "isYAxisHidden", value: function (t) { var e = this.w, i = e.config.yaxis[t]; if (!i.show || this.yAxisAllSeriesCollapsed(t)) return !0; if (!i.showForNullSeries) { var a = e.globals.seriesYAxisMap[t], s = new y(this.ctx); return a.every((function (t) { return s.isSeriesNull(t) })) } return !1 } }, { key: "getYAxisForeColor", value: function (t, e) { var i = this.w; return Array.isArray(t) && i.globals.yAxisScale[e] && this.ctx.theme.pushExtraColors(t, i.globals.yAxisScale[e].result.length, !1), t } }, { key: "drawYAxisTicks", value: function (t, e, i, a, s, r, o) { var n = this.w, l = new m(this.ctx), h = n.globals.translateY + n.config.yaxis[s].labels.offsetY; if (n.globals.isBarHorizontal ? h = 0 : "heatmap" === n.config.chart.type && (h += r / 2), a.show && e > 0) { !0 === n.config.yaxis[s].opposite && (t += a.width); for (var c = e; c >= 0; c--) { var d = l.drawLine(t + i.offsetX - a.width + a.offsetX, h + a.offsetY, t + i.offsetX + a.offsetX, h + a.offsetY, a.color); o.add(d), h += r } } } }]), t }(), L = function () { function t(e) { a(this, t), this.w = e.w, this.annoCtx = e, this.helpers = new w(this.annoCtx), this.axesUtils = new C(this.annoCtx) } return r(t, [{ key: "addYaxisAnnotation", value: function (t, e, i) { var a, s = this.w, r = t.strokeDashArray, o = this.helpers.getY1Y2("y1", t), n = o.yP, l = o.clipped, h = !0, c = !1, d = t.label.text; if (null === t.y2 || void 0 === t.y2) { if (!l) { c = !0; var g = this.annoCtx.graphics.drawLine(0 + t.offsetX, n + t.offsetY, this._getYAxisAnnotationWidth(t), n + t.offsetY, t.borderColor, r, t.borderWidth); e.appendChild(g.node), t.id && g.node.classList.add(t.id) } } else { if (a = (o = this.helpers.getY1Y2("y2", t)).yP, h = o.clipped, a > n) { var u = n; n = a, a = u } if (!l || !h) { c = !0; var p = this.annoCtx.graphics.drawRect(0 + t.offsetX, a + t.offsetY, this._getYAxisAnnotationWidth(t), n - a, 0, t.fillColor, t.opacity, 1, t.borderColor, r); p.node.classList.add("apexcharts-annotation-rect"), p.attr("clip-path", "url(#gridRectMask".concat(s.globals.cuid, ")")), e.appendChild(p.node), t.id && p.node.classList.add(t.id) } } if (c) { var f = "right" === t.label.position ? s.globals.gridWidth : "center" === t.label.position ? s.globals.gridWidth / 2 : 0, x = this.annoCtx.graphics.drawText({ x: f + t.label.offsetX, y: (null != a ? a : n) + t.label.offsetY - 3, text: d, textAnchor: t.label.textAnchor, fontSize: t.label.style.fontSize, fontFamily: t.label.style.fontFamily, fontWeight: t.label.style.fontWeight, foreColor: t.label.style.color, cssClass: "apexcharts-yaxis-annotation-label ".concat(t.label.style.cssClass, " ").concat(t.id ? t.id : "") }); x.attr({ rel: i }), e.appendChild(x.node) } } }, { key: "_getYAxisAnnotationWidth", value: function (t) { var e = this.w; e.globals.gridWidth; return (t.width.indexOf("%") > -1 ? e.globals.gridWidth * parseInt(t.width, 10) / 100 : parseInt(t.width, 10)) + t.offsetX } }, { key: "drawYAxisAnnotations", value: function () { var t = this, e = this.w, i = this.annoCtx.graphics.group({ class: "apexcharts-yaxis-annotations" }); return e.config.annotations.yaxis.forEach((function (e, a) { e.yAxisIndex = t.axesUtils.translateYAxisIndex(e.yAxisIndex), t.axesUtils.isYAxisHidden(e.yAxisIndex) && t.axesUtils.yAxisAllSeriesCollapsed(e.yAxisIndex) || t.addYaxisAnnotation(e, i.node, a) })), i } }]), t }(), P = function () { function t(e) { a(this, t), this.w = e.w, this.annoCtx = e, this.helpers = new w(this.annoCtx) } return r(t, [{ key: "addPointAnnotation", value: function (t, e, i) { if (!(this.w.globals.collapsedSeriesIndices.indexOf(t.seriesIndex) > -1)) { var a = this.helpers.getX1X2("x1", t), s = a.x, r = a.clipped, o = (a = this.helpers.getY1Y2("y1", t)).yP, n = a.clipped; if (x.isNumber(s) && !n && !r) { var l = { pSize: t.marker.size, pointStrokeWidth: t.marker.strokeWidth, pointFillColor: t.marker.fillColor, pointStrokeColor: t.marker.strokeColor, shape: t.marker.shape, pRadius: t.marker.radius, class: "apexcharts-point-annotation-marker ".concat(t.marker.cssClass, " ").concat(t.id ? t.id : "") }, h = this.annoCtx.graphics.drawMarker(s + t.marker.offsetX, o + t.marker.offsetY, l); e.appendChild(h.node); var c = t.label.text ? t.label.text : "", d = this.annoCtx.graphics.drawText({ x: s + t.label.offsetX, y: o + t.label.offsetY - t.marker.size - parseFloat(t.label.style.fontSize) / 1.6, text: c, textAnchor: t.label.textAnchor, fontSize: t.label.style.fontSize, fontFamily: t.label.style.fontFamily, fontWeight: t.label.style.fontWeight, foreColor: t.label.style.color, cssClass: "apexcharts-point-annotation-label ".concat(t.label.style.cssClass, " ").concat(t.id ? t.id : "") }); if (d.attr({ rel: i }), e.appendChild(d.node), t.customSVG.SVG) { var g = this.annoCtx.graphics.group({ class: "apexcharts-point-annotations-custom-svg " + t.customSVG.cssClass }); g.attr({ transform: "translate(".concat(s + t.customSVG.offsetX, ", ").concat(o + t.customSVG.offsetY, ")") }), g.node.innerHTML = t.customSVG.SVG, e.appendChild(g.node) } if (t.image.path) { var u = t.image.width ? t.image.width : 20, p = t.image.height ? t.image.height : 20; h = this.annoCtx.addImage({ x: s + t.image.offsetX - u / 2, y: o + t.image.offsetY - p / 2, width: u, height: p, path: t.image.path, appendTo: ".apexcharts-point-annotations" }) } t.mouseEnter && h.node.addEventListener("mouseenter", t.mouseEnter.bind(this, t)), t.mouseLeave && h.node.addEventListener("mouseleave", t.mouseLeave.bind(this, t)), t.click && h.node.addEventListener("click", t.click.bind(this, t)) } } } }, { key: "drawPointAnnotations", value: function () { var t = this, e = this.w, i = this.annoCtx.graphics.group({ class: "apexcharts-point-annotations" }); return e.config.annotations.points.map((function (e, a) { t.addPointAnnotation(e, i.node, a) })), i } }]), t }(); var M = { name: "en", options: { months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], shortMonths: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], shortDays: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], toolbar: { exportToSVG: "Download SVG", exportToPNG: "Download PNG", exportToCSV: "Download CSV", menu: "Menu", selection: "Selection", selectionZoom: "Selection Zoom", zoomIn: "Zoom In", zoomOut: "Zoom Out", pan: "Panning", reset: "Reset Zoom" } } }, I = function () { function t() { a(this, t), this.yAxis = { show: !0, showAlways: !1, showForNullSeries: !0, seriesName: void 0, opposite: !1, reversed: !1, logarithmic: !1, logBase: 10, tickAmount: void 0, stepSize: void 0, forceNiceScale: !1, max: void 0, min: void 0, floating: !1, decimalsInFloat: void 0, labels: { show: !0, minWidth: 0, maxWidth: 160, offsetX: 0, offsetY: 0, align: void 0, rotate: 0, padding: 20, style: { colors: [], fontSize: "11px", fontWeight: 400, fontFamily: void 0, cssClass: "" }, formatter: void 0 }, axisBorder: { show: !1, color: "#e0e0e0", width: 1, offsetX: 0, offsetY: 0 }, axisTicks: { show: !1, color: "#e0e0e0", width: 6, offsetX: 0, offsetY: 0 }, title: { text: void 0, rotate: -90, offsetY: 0, offsetX: 0, style: { color: void 0, fontSize: "11px", fontWeight: 900, fontFamily: void 0, cssClass: "" } }, tooltip: { enabled: !1, offsetX: 0 }, crosshairs: { show: !0, position: "front", stroke: { color: "#b6b6b6", width: 1, dashArray: 0 } } }, this.pointAnnotation = { id: void 0, x: 0, y: null, yAxisIndex: 0, seriesIndex: void 0, mouseEnter: void 0, mouseLeave: void 0, click: void 0, marker: { size: 4, fillColor: "#fff", strokeWidth: 2, strokeColor: "#333", shape: "circle", offsetX: 0, offsetY: 0, radius: 2, cssClass: "" }, label: { borderColor: "#c2c2c2", borderWidth: 1, borderRadius: 2, text: void 0, textAnchor: "middle", offsetX: 0, offsetY: 0, mouseEnter: void 0, mouseLeave: void 0, click: void 0, style: { background: "#fff", color: void 0, fontSize: "11px", fontFamily: void 0, fontWeight: 400, cssClass: "", padding: { left: 5, right: 5, top: 2, bottom: 2 } } }, customSVG: { SVG: void 0, cssClass: void 0, offsetX: 0, offsetY: 0 }, image: { path: void 0, width: 20, height: 20, offsetX: 0, offsetY: 0 } }, this.yAxisAnnotation = { id: void 0, y: 0, y2: null, strokeDashArray: 1, fillColor: "#c2c2c2", borderColor: "#c2c2c2", borderWidth: 1, opacity: .3, offsetX: 0, offsetY: 0, width: "100%", yAxisIndex: 0, label: { borderColor: "#c2c2c2", borderWidth: 1, borderRadius: 2, text: void 0, textAnchor: "end", position: "right", offsetX: 0, offsetY: -3, mouseEnter: void 0, mouseLeave: void 0, click: void 0, style: { background: "#fff", color: void 0, fontSize: "11px", fontFamily: void 0, fontWeight: 400, cssClass: "", padding: { left: 5, right: 5, top: 2, bottom: 2 } } } }, this.xAxisAnnotation = { id: void 0, x: 0, x2: null, strokeDashArray: 1, fillColor: "#c2c2c2", borderColor: "#c2c2c2", borderWidth: 1, opacity: .3, offsetX: 0, offsetY: 0, label: { borderColor: "#c2c2c2", borderWidth: 1, borderRadius: 2, text: void 0, textAnchor: "middle", orientation: "vertical", position: "top", offsetX: 0, offsetY: 0, mouseEnter: void 0, mouseLeave: void 0, click: void 0, style: { background: "#fff", color: void 0, fontSize: "11px", fontFamily: void 0, fontWeight: 400, cssClass: "", padding: { left: 5, right: 5, top: 2, bottom: 2 } } } }, this.text = { x: 0, y: 0, text: "", textAnchor: "start", foreColor: void 0, fontSize: "13px", fontFamily: void 0, fontWeight: 400, appendTo: ".apexcharts-annotations", backgroundColor: "transparent", borderColor: "#c2c2c2", borderRadius: 0, borderWidth: 0, paddingLeft: 4, paddingRight: 4, paddingTop: 2, paddingBottom: 2 } } return r(t, [{ key: "init", value: function () { return { annotations: { yaxis: [this.yAxisAnnotation], xaxis: [this.xAxisAnnotation], points: [this.pointAnnotation], texts: [], images: [], shapes: [] }, chart: { animations: { enabled: !0, easing: "easeinout", speed: 800, animateGradually: { delay: 150, enabled: !0 }, dynamicAnimation: { enabled: !0, speed: 350 } }, background: "transparent", locales: [M], defaultLocale: "en", dropShadow: { enabled: !1, enabledOnSeries: void 0, top: 2, left: 2, blur: 4, color: "#000", opacity: .35 }, events: { animationEnd: void 0, beforeMount: void 0, mounted: void 0, updated: void 0, click: void 0, mouseMove: void 0, mouseLeave: void 0, xAxisLabelClick: void 0, legendClick: void 0, markerClick: void 0, selection: void 0, dataPointSelection: void 0, dataPointMouseEnter: void 0, dataPointMouseLeave: void 0, beforeZoom: void 0, beforeResetZoom: void 0, zoomed: void 0, scrolled: void 0, brushScrolled: void 0 }, foreColor: "#373d3f", fontFamily: "Helvetica, Arial, sans-serif", height: "auto", parentHeightOffset: 15, redrawOnParentResize: !0, redrawOnWindowResize: !0, id: void 0, group: void 0, nonce: void 0, offsetX: 0, offsetY: 0, selection: { enabled: !1, type: "x", fill: { color: "#24292e", opacity: .1 }, stroke: { width: 1, color: "#24292e", opacity: .4, dashArray: 3 }, xaxis: { min: void 0, max: void 0 }, yaxis: { min: void 0, max: void 0 } }, sparkline: { enabled: !1 }, brush: { enabled: !1, autoScaleYaxis: !0, target: void 0, targets: void 0 }, stacked: !1, stackOnlyBar: !0, stackType: "normal", toolbar: { show: !0, offsetX: 0, offsetY: 0, tools: { download: !0, selection: !0, zoom: !0, zoomin: !0, zoomout: !0, pan: !0, reset: !0, customIcons: [] }, export: { csv: { filename: void 0, columnDelimiter: ",", headerCategory: "category", headerValue: "value", dateFormatter: function (t) { return new Date(t).toDateString() } }, png: { filename: void 0 }, svg: { filename: void 0 } }, autoSelected: "zoom" }, type: "line", width: "100%", zoom: { enabled: !0, type: "x", autoScaleYaxis: !1, zoomedArea: { fill: { color: "#90CAF9", opacity: .4 }, stroke: { color: "#0D47A1", opacity: .4, width: 1 } } } }, plotOptions: { line: { isSlopeChart: !1 }, area: { fillTo: "origin" }, bar: { horizontal: !1, columnWidth: "70%", barHeight: "70%", distributed: !1, borderRadius: 0, borderRadiusApplication: "around", borderRadiusWhenStacked: "last", rangeBarOverlap: !0, rangeBarGroupRows: !1, hideZeroBarsWhenGrouped: !1, isDumbbell: !1, dumbbellColors: void 0, isFunnel: !1, isFunnel3d: !0, colors: { ranges: [], backgroundBarColors: [], backgroundBarOpacity: 1, backgroundBarRadius: 0 }, dataLabels: { position: "top", maxItems: 100, hideOverflowingLabels: !0, orientation: "horizontal", total: { enabled: !1, formatter: void 0, offsetX: 0, offsetY: 0, style: { color: "#373d3f", fontSize: "12px", fontFamily: void 0, fontWeight: 600 } } } }, bubble: { zScaling: !0, minBubbleRadius: void 0, maxBubbleRadius: void 0 }, candlestick: { colors: { upward: "#00B746", downward: "#EF403C" }, wick: { useFillColor: !0 } }, boxPlot: { colors: { upper: "#00E396", lower: "#008FFB" } }, heatmap: { radius: 2, enableShades: !0, shadeIntensity: .5, reverseNegativeShade: !1, distributed: !1, useFillColorAsStroke: !1, colorScale: { inverse: !1, ranges: [], min: void 0, max: void 0 } }, treemap: { enableShades: !0, shadeIntensity: .5, distributed: !1, reverseNegativeShade: !1, useFillColorAsStroke: !1, borderRadius: 4, dataLabels: { format: "scale" }, colorScale: { inverse: !1, ranges: [], min: void 0, max: void 0 } }, radialBar: { inverseOrder: !1, startAngle: 0, endAngle: 360, offsetX: 0, offsetY: 0, hollow: { margin: 5, size: "50%", background: "transparent", image: void 0, imageWidth: 150, imageHeight: 150, imageOffsetX: 0, imageOffsetY: 0, imageClipped: !0, position: "front", dropShadow: { enabled: !1, top: 0, left: 0, blur: 3, color: "#000", opacity: .5 } }, track: { show: !0, startAngle: void 0, endAngle: void 0, background: "#f2f2f2", strokeWidth: "97%", opacity: 1, margin: 5, dropShadow: { enabled: !1, top: 0, left: 0, blur: 3, color: "#000", opacity: .5 } }, dataLabels: { show: !0, name: { show: !0, fontSize: "16px", fontFamily: void 0, fontWeight: 600, color: void 0, offsetY: 0, formatter: function (t) { return t } }, value: { show: !0, fontSize: "14px", fontFamily: void 0, fontWeight: 400, color: void 0, offsetY: 16, formatter: function (t) { return t + "%" } }, total: { show: !1, label: "Total", fontSize: "16px", fontWeight: 600, fontFamily: void 0, color: void 0, formatter: function (t) { return t.globals.seriesTotals.reduce((function (t, e) { return t + e }), 0) / t.globals.series.length + "%" } } }, barLabels: { enabled: !1, margin: 5, useSeriesColors: !0, fontFamily: void 0, fontWeight: 600, fontSize: "16px", formatter: function (t) { return t }, onClick: void 0 } }, pie: { customScale: 1, offsetX: 0, offsetY: 0, startAngle: 0, endAngle: 360, expandOnClick: !0, dataLabels: { offset: 0, minAngleToShowLabel: 10 }, donut: { size: "65%", background: "transparent", labels: { show: !1, name: { show: !0, fontSize: "16px", fontFamily: void 0, fontWeight: 600, color: void 0, offsetY: -10, formatter: function (t) { return t } }, value: { show: !0, fontSize: "20px", fontFamily: void 0, fontWeight: 400, color: void 0, offsetY: 10, formatter: function (t) { return t } }, total: { show: !1, showAlways: !1, label: "Total", fontSize: "16px", fontWeight: 400, fontFamily: void 0, color: void 0, formatter: function (t) { return t.globals.seriesTotals.reduce((function (t, e) { return t + e }), 0) } } } } }, polarArea: { rings: { strokeWidth: 1, strokeColor: "#e8e8e8" }, spokes: { strokeWidth: 1, connectorColors: "#e8e8e8" } }, radar: { size: void 0, offsetX: 0, offsetY: 0, polygons: { strokeWidth: 1, strokeColors: "#e8e8e8", connectorColors: "#e8e8e8", fill: { colors: void 0 } } } }, colors: void 0, dataLabels: { enabled: !0, enabledOnSeries: void 0, formatter: function (t) { return null !== t ? t : "" }, textAnchor: "middle", distributed: !1, offsetX: 0, offsetY: 0, style: { fontSize: "12px", fontFamily: void 0, fontWeight: 600, colors: void 0 }, background: { enabled: !0, foreColor: "#fff", borderRadius: 2, padding: 4, opacity: .9, borderWidth: 1, borderColor: "#fff", dropShadow: { enabled: !1, top: 1, left: 1, blur: 1, color: "#000", opacity: .45 } }, dropShadow: { enabled: !1, top: 1, left: 1, blur: 1, color: "#000", opacity: .45 } }, fill: { type: "solid", colors: void 0, opacity: .85, gradient: { shade: "dark", type: "horizontal", shadeIntensity: .5, gradientToColors: void 0, inverseColors: !0, opacityFrom: 1, opacityTo: 1, stops: [0, 50, 100], colorStops: [] }, image: { src: [], width: void 0, height: void 0 }, pattern: { style: "squares", width: 6, height: 6, strokeWidth: 2 } }, forecastDataPoints: { count: 0, fillOpacity: .5, strokeWidth: void 0, dashArray: 4 }, grid: { show: !0, borderColor: "#e0e0e0", strokeDashArray: 0, position: "back", xaxis: { lines: { show: !1 } }, yaxis: { lines: { show: !0 } }, row: { colors: void 0, opacity: .5 }, column: { colors: void 0, opacity: .5 }, padding: { top: 0, right: 10, bottom: 0, left: 12 } }, labels: [], legend: { show: !0, showForSingleSeries: !1, showForNullSeries: !0, showForZeroSeries: !0, floating: !1, position: "bottom", horizontalAlign: "center", inverseOrder: !1, fontSize: "12px", fontFamily: void 0, fontWeight: 400, width: void 0, height: void 0, formatter: void 0, tooltipHoverFormatter: void 0, offsetX: -20, offsetY: 4, customLegendItems: [], labels: { colors: void 0, useSeriesColors: !1 }, markers: { width: 12, height: 12, strokeWidth: 0, fillColors: void 0, strokeColor: "#fff", radius: 12, customHTML: void 0, offsetX: 0, offsetY: 0, onClick: void 0 }, itemMargin: { horizontal: 5, vertical: 2 }, onItemClick: { toggleDataSeries: !0 }, onItemHover: { highlightDataSeries: !0 } }, markers: { discrete: [], size: 0, colors: void 0, strokeColors: "#fff", strokeWidth: 2, strokeOpacity: .9, strokeDashArray: 0, fillOpacity: 1, shape: "circle", width: 8, height: 8, radius: 2, offsetX: 0, offsetY: 0, onClick: void 0, onDblClick: void 0, showNullDataPoints: !0, hover: { size: void 0, sizeOffset: 3 } }, noData: { text: void 0, align: "center", verticalAlign: "middle", offsetX: 0, offsetY: 0, style: { color: void 0, fontSize: "14px", fontFamily: void 0 } }, responsive: [], series: void 0, states: { normal: { filter: { type: "none", value: 0 } }, hover: { filter: { type: "lighten", value: .1 } }, active: { allowMultipleDataPointsSelection: !1, filter: { type: "darken", value: .5 } } }, title: { text: void 0, align: "left", margin: 5, offsetX: 0, offsetY: 0, floating: !1, style: { fontSize: "14px", fontWeight: 900, fontFamily: void 0, color: void 0 } }, subtitle: { text: void 0, align: "left", margin: 5, offsetX: 0, offsetY: 30, floating: !1, style: { fontSize: "12px", fontWeight: 400, fontFamily: void 0, color: void 0 } }, stroke: { show: !0, curve: "smooth", lineCap: "butt", width: 2, colors: void 0, dashArray: 0, fill: { type: "solid", colors: void 0, opacity: .85, gradient: { shade: "dark", type: "horizontal", shadeIntensity: .5, gradientToColors: void 0, inverseColors: !0, opacityFrom: 1, opacityTo: 1, stops: [0, 50, 100], colorStops: [] } } }, tooltip: { enabled: !0, enabledOnSeries: void 0, shared: !0, hideEmptySeries: !1, followCursor: !1, intersect: !1, inverseOrder: !1, custom: void 0, fillSeriesColor: !1, theme: "light", cssClass: "", style: { fontSize: "12px", fontFamily: void 0 }, onDatasetHover: { highlightDataSeries: !1 }, x: { show: !0, format: "dd MMM", formatter: void 0 }, y: { formatter: void 0, title: { formatter: function (t) { return t ? t + ": " : "" } } }, z: { formatter: void 0, title: "Size: " }, marker: { show: !0, fillColors: void 0 }, items: { display: "flex" }, fixed: { enabled: !1, position: "topRight", offsetX: 0, offsetY: 0 } }, xaxis: { type: "category", categories: [], convertedCatToNumeric: !1, offsetX: 0, offsetY: 0, overwriteCategories: void 0, labels: { show: !0, rotate: -45, rotateAlways: !1, hideOverlappingLabels: !0, trim: !1, minHeight: void 0, maxHeight: 120, showDuplicates: !0, style: { colors: [], fontSize: "12px", fontWeight: 400, fontFamily: void 0, cssClass: "" }, offsetX: 0, offsetY: 0, format: void 0, formatter: void 0, datetimeUTC: !0, datetimeFormatter: { year: "yyyy", month: "MMM 'yy", day: "dd MMM", hour: "HH:mm", minute: "HH:mm:ss", second: "HH:mm:ss" } }, group: { groups: [], style: { colors: [], fontSize: "12px", fontWeight: 400, fontFamily: void 0, cssClass: "" } }, axisBorder: { show: !0, color: "#e0e0e0", width: "100%", height: 1, offsetX: 0, offsetY: 0 }, axisTicks: { show: !0, color: "#e0e0e0", height: 6, offsetX: 0, offsetY: 0 }, stepSize: void 0, tickAmount: void 0, tickPlacement: "on", min: void 0, max: void 0, range: void 0, floating: !1, decimalsInFloat: void 0, position: "bottom", title: { text: void 0, offsetX: 0, offsetY: 0, style: { color: void 0, fontSize: "12px", fontWeight: 900, fontFamily: void 0, cssClass: "" } }, crosshairs: { show: !0, width: 1, position: "back", opacity: .9, stroke: { color: "#b6b6b6", width: 1, dashArray: 3 }, fill: { type: "solid", color: "#B1B9C4", gradient: { colorFrom: "#D8E3F0", colorTo: "#BED1E6", stops: [0, 100], opacityFrom: .4, opacityTo: .5 } }, dropShadow: { enabled: !1, left: 0, top: 0, blur: 1, opacity: .4 } }, tooltip: { enabled: !0, offsetY: 0, formatter: void 0, style: { fontSize: "12px", fontFamily: void 0 } } }, yaxis: this.yAxis, theme: { mode: "light", palette: "palette1", monochrome: { enabled: !1, color: "#008FFB", shadeTo: "light", shadeIntensity: .65 } } } } }]), t }(), T = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w, this.graphics = new m(this.ctx), this.w.globals.isBarHorizontal && (this.invertAxis = !0), this.helpers = new w(this), this.xAxisAnnotations = new k(this), this.yAxisAnnotations = new L(this), this.pointsAnnotations = new P(this), this.w.globals.isBarHorizontal && this.w.config.yaxis[0].reversed && (this.inversedReversedAxis = !0), this.xDivision = this.w.globals.gridWidth / this.w.globals.dataPoints } return r(t, [{ key: "drawAxesAnnotations", value: function () { var t = this.w; if (t.globals.axisCharts) { for (var e = this.yAxisAnnotations.drawYAxisAnnotations(), i = this.xAxisAnnotations.drawXAxisAnnotations(), a = this.pointsAnnotations.drawPointAnnotations(), s = t.config.chart.animations.enabled, r = [e, i, a], o = [i.node, e.node, a.node], n = 0; n < 3; n++)t.globals.dom.elGraphical.add(r[n]), !s || t.globals.resized || t.globals.dataChanged || "scatter" !== t.config.chart.type && "bubble" !== t.config.chart.type && t.globals.dataPoints > 1 && o[n].classList.add("apexcharts-element-hidden"), t.globals.delayedElements.push({ el: o[n], index: 0 }); this.helpers.annotationsBackground() } } }, { key: "drawImageAnnos", value: function () { var t = this; this.w.config.annotations.images.map((function (e, i) { t.addImage(e, i) })) } }, { key: "drawTextAnnos", value: function () { var t = this; this.w.config.annotations.texts.map((function (e, i) { t.addText(e, i) })) } }, { key: "addXaxisAnnotation", value: function (t, e, i) { this.xAxisAnnotations.addXaxisAnnotation(t, e, i) } }, { key: "addYaxisAnnotation", value: function (t, e, i) { this.yAxisAnnotations.addYaxisAnnotation(t, e, i) } }, { key: "addPointAnnotation", value: function (t, e, i) { this.pointsAnnotations.addPointAnnotation(t, e, i) } }, { key: "addText", value: function (t, e) { var i = t.x, a = t.y, s = t.text, r = t.textAnchor, o = t.foreColor, n = t.fontSize, l = t.fontFamily, h = t.fontWeight, c = t.cssClass, d = t.backgroundColor, g = t.borderWidth, u = t.strokeDashArray, p = t.borderRadius, f = t.borderColor, x = t.appendTo, b = void 0 === x ? ".apexcharts-svg" : x, v = t.paddingLeft, m = void 0 === v ? 4 : v, y = t.paddingRight, w = void 0 === y ? 4 : y, k = t.paddingBottom, A = void 0 === k ? 2 : k, S = t.paddingTop, C = void 0 === S ? 2 : S, L = this.w, P = this.graphics.drawText({ x: i, y: a, text: s, textAnchor: r || "start", fontSize: n || "12px", fontWeight: h || "regular", fontFamily: l || L.config.chart.fontFamily, foreColor: o || L.config.chart.foreColor, cssClass: c }), M = L.globals.dom.baseEl.querySelector(b); M && M.appendChild(P.node); var I = P.bbox(); if (s) { var T = this.graphics.drawRect(I.x - m, I.y - C, I.width + m + w, I.height + A + C, p, d || "transparent", 1, g, f, u); M.insertBefore(T.node, P.node) } } }, { key: "addImage", value: function (t, e) { var i = this.w, a = t.path, s = t.x, r = void 0 === s ? 0 : s, o = t.y, n = void 0 === o ? 0 : o, l = t.width, h = void 0 === l ? 20 : l, c = t.height, d = void 0 === c ? 20 : c, g = t.appendTo, u = void 0 === g ? ".apexcharts-svg" : g, p = i.globals.dom.Paper.image(a); p.size(h, d).move(r, n); var f = i.globals.dom.baseEl.querySelector(u); return f && f.appendChild(p.node), p } }, { key: "addXaxisAnnotationExternal", value: function (t, e, i) { return this.addAnnotationExternal({ params: t, pushToMemory: e, context: i, type: "xaxis", contextMethod: i.addXaxisAnnotation }), i } }, { key: "addYaxisAnnotationExternal", value: function (t, e, i) { return this.addAnnotationExternal({ params: t, pushToMemory: e, context: i, type: "yaxis", contextMethod: i.addYaxisAnnotation }), i } }, { key: "addPointAnnotationExternal", value: function (t, e, i) { return void 0 === this.invertAxis && (this.invertAxis = i.w.globals.isBarHorizontal), this.addAnnotationExternal({ params: t, pushToMemory: e, context: i, type: "point", contextMethod: i.addPointAnnotation }), i } }, { key: "addAnnotationExternal", value: function (t) { var e = t.params, i = t.pushToMemory, a = t.context, s = t.type, r = t.contextMethod, o = a, n = o.w, l = n.globals.dom.baseEl.querySelector(".apexcharts-".concat(s, "-annotations")), h = l.childNodes.length + 1, c = new I, d = Object.assign({}, "xaxis" === s ? c.xAxisAnnotation : "yaxis" === s ? c.yAxisAnnotation : c.pointAnnotation), g = x.extend(d, e); switch (s) { case "xaxis": this.addXaxisAnnotation(g, l, h); break; case "yaxis": this.addYaxisAnnotation(g, l, h); break; case "point": this.addPointAnnotation(g, l, h) }var u = n.globals.dom.baseEl.querySelector(".apexcharts-".concat(s, "-annotations .apexcharts-").concat(s, "-annotation-label[rel='").concat(h, "']")), p = this.helpers.addBackgroundToAnno(u, g); return p && l.insertBefore(p.node, u), i && n.globals.memory.methodsToExec.push({ context: o, id: g.id ? g.id : x.randomId(), method: r, label: "addAnnotation", params: e }), a } }, { key: "clearAnnotations", value: function (t) { var e = t.w, i = e.globals.dom.baseEl.querySelectorAll(".apexcharts-yaxis-annotations, .apexcharts-xaxis-annotations, .apexcharts-point-annotations"); e.globals.memory.methodsToExec.map((function (t, i) { "addText" !== t.label && "addAnnotation" !== t.label || e.globals.memory.methodsToExec.splice(i, 1) })), i = x.listToArray(i), Array.prototype.forEach.call(i, (function (t) { for (; t.firstChild;)t.removeChild(t.firstChild) })) } }, { key: "removeAnnotation", value: function (t, e) { var i = t.w, a = i.globals.dom.baseEl.querySelectorAll(".".concat(e)); a && (i.globals.memory.methodsToExec.map((function (t, a) { t.id === e && i.globals.memory.methodsToExec.splice(a, 1) })), Array.prototype.forEach.call(a, (function (t) { t.parentElement.removeChild(t) }))) } }]), t }(), z = function (t) { var e, i = t.isTimeline, a = t.ctx, s = t.seriesIndex, r = t.dataPointIndex, o = t.y1, n = t.y2, l = t.w, h = l.globals.seriesRangeStart[s][r], c = l.globals.seriesRangeEnd[s][r], d = l.globals.labels[r], g = l.config.series[s].name ? l.config.series[s].name : "", u = l.globals.ttKeyFormatter, p = l.config.tooltip.y.title.formatter, f = { w: l, seriesIndex: s, dataPointIndex: r, start: h, end: c }; ("function" == typeof p && (g = p(g, f)), null !== (e = l.config.series[s].data[r]) && void 0 !== e && e.x && (d = l.config.series[s].data[r].x), i) || "datetime" === l.config.xaxis.type && (d = new S(a).xLabelFormat(l.globals.ttKeyFormatter, d, d, { i: void 0, dateFormatter: new A(a).formatDate, w: l })); "function" == typeof u && (d = u(d, f)), Number.isFinite(o) && Number.isFinite(n) && (h = o, c = n); var x = "", b = "", v = l.globals.colors[s]; if (void 0 === l.config.tooltip.x.formatter) if ("datetime" === l.config.xaxis.type) { var m = new A(a); x = m.formatDate(m.getDate(h), l.config.tooltip.x.format), b = m.formatDate(m.getDate(c), l.config.tooltip.x.format) } else x = h, b = c; else x = l.config.tooltip.x.formatter(h), b = l.config.tooltip.x.formatter(c); return { start: h, end: c, startVal: x, endVal: b, ylabel: d, color: v, seriesName: g } }, X = function (t) { var e = t.color, i = t.seriesName, a = t.ylabel, s = t.start, r = t.end, o = t.seriesIndex, n = t.dataPointIndex, l = t.ctx.tooltip.tooltipLabels.getFormatters(o); s = l.yLbFormatter(s), r = l.yLbFormatter(r); var h = l.yLbFormatter(t.w.globals.series[o][n]), c = '\n '.concat(s, '\n - \n ').concat(r, "\n "); return '
' + (i || "") + '
' + a + ": " + (t.w.globals.comboCharts ? "rangeArea" === t.w.config.series[o].type || "rangeBar" === t.w.config.series[o].type ? c : "".concat(h, "") : c) + "
" }, E = function () { function t(e) { a(this, t), this.opts = e } return r(t, [{ key: "hideYAxis", value: function () { this.opts.yaxis[0].show = !1, this.opts.yaxis[0].title.text = "", this.opts.yaxis[0].axisBorder.show = !1, this.opts.yaxis[0].axisTicks.show = !1, this.opts.yaxis[0].floating = !0 } }, { key: "line", value: function () { return { chart: { animations: { easing: "swing" } }, dataLabels: { enabled: !1 }, stroke: { width: 5, curve: "straight" }, markers: { size: 0, hover: { sizeOffset: 6 } }, xaxis: { crosshairs: { width: 1 } } } } }, { key: "sparkline", value: function (t) { this.hideYAxis(); return x.extend(t, { grid: { show: !1, padding: { left: 0, right: 0, top: 0, bottom: 0 } }, legend: { show: !1 }, xaxis: { labels: { show: !1 }, tooltip: { enabled: !1 }, axisBorder: { show: !1 }, axisTicks: { show: !1 } }, chart: { toolbar: { show: !1 }, zoom: { enabled: !1 } }, dataLabels: { enabled: !1 } }) } }, { key: "slope", value: function () { return this.hideYAxis(), { chart: { toolbar: { show: !1 }, zoom: { enabled: !1 } }, dataLabels: { enabled: !0, formatter: function (t, e) { var i = e.w.config.series[e.seriesIndex].name; return null !== t ? i + ": " + t : "" }, background: { enabled: !1 }, offsetX: -5 }, grid: { xaxis: { lines: { show: !0 } }, yaxis: { lines: { show: !1 } } }, xaxis: { position: "top", labels: { style: { fontSize: 14, fontWeight: 900 } }, tooltip: { enabled: !1 }, crosshairs: { show: !1 } }, markers: { size: 8, hover: { sizeOffset: 1 } }, legend: { show: !1 }, tooltip: { shared: !1, intersect: !0, followCursor: !0 }, stroke: { width: 5, curve: "straight" } } } }, { key: "bar", value: function () { return { chart: { stacked: !1, animations: { easing: "swing" } }, plotOptions: { bar: { dataLabels: { position: "center" } } }, dataLabels: { style: { colors: ["#fff"] }, background: { enabled: !1 } }, stroke: { width: 0, lineCap: "round" }, fill: { opacity: .85 }, legend: { markers: { shape: "square", radius: 2, size: 8 } }, tooltip: { shared: !1, intersect: !0 }, xaxis: { tooltip: { enabled: !1 }, tickPlacement: "between", crosshairs: { width: "barWidth", position: "back", fill: { type: "gradient" }, dropShadow: { enabled: !1 }, stroke: { width: 0 } } } } } }, { key: "funnel", value: function () { return this.hideYAxis(), e(e({}, this.bar()), {}, { chart: { animations: { easing: "linear", speed: 800, animateGradually: { enabled: !1 } } }, plotOptions: { bar: { horizontal: !0, borderRadiusApplication: "around", borderRadius: 0, dataLabels: { position: "center" } } }, grid: { show: !1, padding: { left: 0, right: 0 } }, xaxis: { labels: { show: !1 }, tooltip: { enabled: !1 }, axisBorder: { show: !1 }, axisTicks: { show: !1 } } }) } }, { key: "candlestick", value: function () { var t = this; return { stroke: { width: 1, colors: ["#333"] }, fill: { opacity: 1 }, dataLabels: { enabled: !1 }, tooltip: { shared: !0, custom: function (e) { var i = e.seriesIndex, a = e.dataPointIndex, s = e.w; return t._getBoxTooltip(s, i, a, ["Open", "High", "", "Low", "Close"], "candlestick") } }, states: { active: { filter: { type: "none" } } }, xaxis: { crosshairs: { width: 1 } } } } }, { key: "boxPlot", value: function () { var t = this; return { chart: { animations: { dynamicAnimation: { enabled: !1 } } }, stroke: { width: 1, colors: ["#24292e"] }, dataLabels: { enabled: !1 }, tooltip: { shared: !0, custom: function (e) { var i = e.seriesIndex, a = e.dataPointIndex, s = e.w; return t._getBoxTooltip(s, i, a, ["Minimum", "Q1", "Median", "Q3", "Maximum"], "boxPlot") } }, markers: { size: 5, strokeWidth: 1, strokeColors: "#111" }, xaxis: { crosshairs: { width: 1 } } } } }, { key: "rangeBar", value: function () { return { chart: { animations: { animateGradually: !1 } }, stroke: { width: 0, lineCap: "square" }, plotOptions: { bar: { borderRadius: 0, dataLabels: { position: "center" } } }, dataLabels: { enabled: !1, formatter: function (t, e) { e.ctx; var i = e.seriesIndex, a = e.dataPointIndex, s = e.w, r = function () { var t = s.globals.seriesRangeStart[i][a]; return s.globals.seriesRangeEnd[i][a] - t }; return s.globals.comboCharts ? "rangeBar" === s.config.series[i].type || "rangeArea" === s.config.series[i].type ? r() : t : r() }, background: { enabled: !1 }, style: { colors: ["#fff"] } }, markers: { size: 10 }, tooltip: { shared: !1, followCursor: !0, custom: function (t) { return t.w.config.plotOptions && t.w.config.plotOptions.bar && t.w.config.plotOptions.bar.horizontal ? function (t) { var i = z(e(e({}, t), {}, { isTimeline: !0 })), a = i.color, s = i.seriesName, r = i.ylabel, o = i.startVal, n = i.endVal; return X(e(e({}, t), {}, { color: a, seriesName: s, ylabel: r, start: o, end: n })) }(t) : function (t) { var i = z(t), a = i.color, s = i.seriesName, r = i.ylabel, o = i.start, n = i.end; return X(e(e({}, t), {}, { color: a, seriesName: s, ylabel: r, start: o, end: n })) }(t) } }, xaxis: { tickPlacement: "between", tooltip: { enabled: !1 }, crosshairs: { stroke: { width: 0 } } } } } }, { key: "dumbbell", value: function (t) { var e, i; return null !== (e = t.plotOptions.bar) && void 0 !== e && e.barHeight || (t.plotOptions.bar.barHeight = 2), null !== (i = t.plotOptions.bar) && void 0 !== i && i.columnWidth || (t.plotOptions.bar.columnWidth = 2), t } }, { key: "area", value: function () { return { stroke: { width: 4, fill: { type: "solid", gradient: { inverseColors: !1, shade: "light", type: "vertical", opacityFrom: .65, opacityTo: .5, stops: [0, 100, 100] } } }, fill: { type: "gradient", gradient: { inverseColors: !1, shade: "light", type: "vertical", opacityFrom: .65, opacityTo: .5, stops: [0, 100, 100] } }, markers: { size: 0, hover: { sizeOffset: 6 } }, tooltip: { followCursor: !1 } } } }, { key: "rangeArea", value: function () { return { stroke: { curve: "straight", width: 0 }, fill: { type: "solid", opacity: .6 }, markers: { size: 0 }, states: { hover: { filter: { type: "none" } }, active: { filter: { type: "none" } } }, tooltip: { intersect: !1, shared: !0, followCursor: !0, custom: function (t) { return function (t) { var i = z(t), a = i.color, s = i.seriesName, r = i.ylabel, o = i.start, n = i.end; return X(e(e({}, t), {}, { color: a, seriesName: s, ylabel: r, start: o, end: n })) }(t) } } } } }, { key: "brush", value: function (t) { return x.extend(t, { chart: { toolbar: { autoSelected: "selection", show: !1 }, zoom: { enabled: !1 } }, dataLabels: { enabled: !1 }, stroke: { width: 1 }, tooltip: { enabled: !1 }, xaxis: { tooltip: { enabled: !1 } } }) } }, { key: "stacked100", value: function (t) { t.dataLabels = t.dataLabels || {}, t.dataLabels.formatter = t.dataLabels.formatter || void 0; var e = t.dataLabels.formatter; return t.yaxis.forEach((function (e, i) { t.yaxis[i].min = 0, t.yaxis[i].max = 100 })), "bar" === t.chart.type && (t.dataLabels.formatter = e || function (t) { return "number" == typeof t && t ? t.toFixed(0) + "%" : t }), t } }, { key: "stackedBars", value: function () { var t = this.bar(); return e(e({}, t), {}, { plotOptions: e(e({}, t.plotOptions), {}, { bar: e(e({}, t.plotOptions.bar), {}, { borderRadiusApplication: "end", borderRadiusWhenStacked: "last" }) }) }) } }, { key: "convertCatToNumeric", value: function (t) { return t.xaxis.convertedCatToNumeric = !0, t } }, { key: "convertCatToNumericXaxis", value: function (t, e, i) { t.xaxis.type = "numeric", t.xaxis.labels = t.xaxis.labels || {}, t.xaxis.labels.formatter = t.xaxis.labels.formatter || function (t) { return x.isNumber(t) ? Math.floor(t) : t }; var a = t.xaxis.labels.formatter, s = t.xaxis.categories && t.xaxis.categories.length ? t.xaxis.categories : t.labels; return i && i.length && (s = i.map((function (t) { return Array.isArray(t) ? t : String(t) }))), s && s.length && (t.xaxis.labels.formatter = function (t) { return x.isNumber(t) ? a(s[Math.floor(t) - 1]) : a(t) }), t.xaxis.categories = [], t.labels = [], t.xaxis.tickAmount = t.xaxis.tickAmount || "dataPoints", t } }, { key: "bubble", value: function () { return { dataLabels: { style: { colors: ["#fff"] } }, tooltip: { shared: !1, intersect: !0 }, xaxis: { crosshairs: { width: 0 } }, fill: { type: "solid", gradient: { shade: "light", inverse: !0, shadeIntensity: .55, opacityFrom: .4, opacityTo: .8 } } } } }, { key: "scatter", value: function () { return { dataLabels: { enabled: !1 }, tooltip: { shared: !1, intersect: !0 }, markers: { size: 6, strokeWidth: 1, hover: { sizeOffset: 2 } } } } }, { key: "heatmap", value: function () { return { chart: { stacked: !1 }, fill: { opacity: 1 }, dataLabels: { style: { colors: ["#fff"] } }, stroke: { colors: ["#fff"] }, tooltip: { followCursor: !0, marker: { show: !1 }, x: { show: !1 } }, legend: { position: "top", markers: { shape: "square", size: 10, offsetY: 2 } }, grid: { padding: { right: 20 } } } } }, { key: "treemap", value: function () { return { chart: { zoom: { enabled: !1 } }, dataLabels: { style: { fontSize: 14, fontWeight: 600, colors: ["#fff"] } }, stroke: { show: !0, width: 2, colors: ["#fff"] }, legend: { show: !1 }, fill: { gradient: { stops: [0, 100] } }, tooltip: { followCursor: !0, x: { show: !1 } }, grid: { padding: { left: 0, right: 0 } }, xaxis: { crosshairs: { show: !1 }, tooltip: { enabled: !1 } } } } }, { key: "pie", value: function () { return { chart: { toolbar: { show: !1 } }, plotOptions: { pie: { donut: { labels: { show: !1 } } } }, dataLabels: { formatter: function (t) { return t.toFixed(1) + "%" }, style: { colors: ["#fff"] }, background: { enabled: !1 }, dropShadow: { enabled: !0 } }, stroke: { colors: ["#fff"] }, fill: { opacity: 1, gradient: { shade: "light", stops: [0, 100] } }, tooltip: { theme: "dark", fillSeriesColor: !0 }, legend: { position: "right" } } } }, { key: "donut", value: function () { return { chart: { toolbar: { show: !1 } }, dataLabels: { formatter: function (t) { return t.toFixed(1) + "%" }, style: { colors: ["#fff"] }, background: { enabled: !1 }, dropShadow: { enabled: !0 } }, stroke: { colors: ["#fff"] }, fill: { opacity: 1, gradient: { shade: "light", shadeIntensity: .35, stops: [80, 100], opacityFrom: 1, opacityTo: 1 } }, tooltip: { theme: "dark", fillSeriesColor: !0 }, legend: { position: "right" } } } }, { key: "polarArea", value: function () { return { chart: { toolbar: { show: !1 } }, dataLabels: { formatter: function (t) { return t.toFixed(1) + "%" }, enabled: !1 }, stroke: { show: !0, width: 2 }, fill: { opacity: .7 }, tooltip: { theme: "dark", fillSeriesColor: !0 }, legend: { position: "right" } } } }, { key: "radar", value: function () { return this.opts.yaxis[0].labels.offsetY = this.opts.yaxis[0].labels.offsetY ? this.opts.yaxis[0].labels.offsetY : 6, { dataLabels: { enabled: !1, style: { fontSize: "11px" } }, stroke: { width: 2 }, markers: { size: 3, strokeWidth: 1, strokeOpacity: 1 }, fill: { opacity: .2 }, tooltip: { shared: !1, intersect: !0, followCursor: !0 }, grid: { show: !1 }, xaxis: { labels: { formatter: function (t) { return t }, style: { colors: ["#a8a8a8"], fontSize: "11px" } }, tooltip: { enabled: !1 }, crosshairs: { show: !1 } } } } }, { key: "radialBar", value: function () { return { chart: { animations: { dynamicAnimation: { enabled: !0, speed: 800 } }, toolbar: { show: !1 } }, fill: { gradient: { shade: "dark", shadeIntensity: .4, inverseColors: !1, type: "diagonal2", opacityFrom: 1, opacityTo: 1, stops: [70, 98, 100] } }, legend: { show: !1, position: "right" }, tooltip: { enabled: !1, fillSeriesColor: !0 } } } }, { key: "_getBoxTooltip", value: function (t, e, i, a, s) { var r = t.globals.seriesCandleO[e][i], o = t.globals.seriesCandleH[e][i], n = t.globals.seriesCandleM[e][i], l = t.globals.seriesCandleL[e][i], h = t.globals.seriesCandleC[e][i]; return t.config.series[e].type && t.config.series[e].type !== s ? '
\n '.concat(t.config.series[e].name ? t.config.series[e].name : "series-" + (e + 1), ": ").concat(t.globals.series[e][i], "\n
") : '
') + "
".concat(a[0], ': ') + r + "
" + "
".concat(a[1], ': ') + o + "
" + (n ? "
".concat(a[2], ': ') + n + "
" : "") + "
".concat(a[3], ': ') + l + "
" + "
".concat(a[4], ': ') + h + "
" } }]), t }(), Y = function () { function t(e) { a(this, t), this.opts = e } return r(t, [{ key: "init", value: function (t) { var e = t.responsiveOverride, a = this.opts, s = new I, r = new E(a); this.chartType = a.chart.type, a = this.extendYAxis(a), a = this.extendAnnotations(a); var o = s.init(), n = {}; if (a && "object" === i(a)) { var l, h, c, d, g, u, p, f, b, v, m = {}; m = -1 !== ["line", "area", "bar", "candlestick", "boxPlot", "rangeBar", "rangeArea", "bubble", "scatter", "heatmap", "treemap", "pie", "polarArea", "donut", "radar", "radialBar"].indexOf(a.chart.type) ? r[a.chart.type]() : r.line(), null !== (l = a.plotOptions) && void 0 !== l && null !== (h = l.bar) && void 0 !== h && h.isFunnel && (m = r.funnel()), a.chart.stacked && "bar" === a.chart.type && (m = r.stackedBars()), null !== (c = a.chart.brush) && void 0 !== c && c.enabled && (m = r.brush(m)), null !== (d = a.plotOptions) && void 0 !== d && null !== (g = d.line) && void 0 !== g && g.isSlopeChart && (m = r.slope()), a.chart.stacked && "100%" === a.chart.stackType && (a = r.stacked100(a)), null !== (u = a.plotOptions) && void 0 !== u && null !== (p = u.bar) && void 0 !== p && p.isDumbbell && (a = r.dumbbell(a)), this.checkForDarkTheme(window.Apex), this.checkForDarkTheme(a), a.xaxis = a.xaxis || window.Apex.xaxis || {}, e || (a.xaxis.convertedCatToNumeric = !1), (null !== (f = (a = this.checkForCatToNumericXAxis(this.chartType, m, a)).chart.sparkline) && void 0 !== f && f.enabled || null !== (b = window.Apex.chart) && void 0 !== b && null !== (v = b.sparkline) && void 0 !== v && v.enabled) && (m = r.sparkline(m)), n = x.extend(o, m) } var y = x.extend(n, window.Apex); return o = x.extend(y, a), o = this.handleUserInputErrors(o) } }, { key: "checkForCatToNumericXAxis", value: function (t, e, i) { var a, s, r = new E(i), o = ("bar" === t || "boxPlot" === t) && (null === (a = i.plotOptions) || void 0 === a || null === (s = a.bar) || void 0 === s ? void 0 : s.horizontal), n = "pie" === t || "polarArea" === t || "donut" === t || "radar" === t || "radialBar" === t || "heatmap" === t, l = "datetime" !== i.xaxis.type && "numeric" !== i.xaxis.type, h = i.xaxis.tickPlacement ? i.xaxis.tickPlacement : e.xaxis && e.xaxis.tickPlacement; return o || n || !l || "between" === h || (i = r.convertCatToNumeric(i)), i } }, { key: "extendYAxis", value: function (t, e) { var i = new I; (void 0 === t.yaxis || !t.yaxis || Array.isArray(t.yaxis) && 0 === t.yaxis.length) && (t.yaxis = {}), t.yaxis.constructor !== Array && window.Apex.yaxis && window.Apex.yaxis.constructor !== Array && (t.yaxis = x.extend(t.yaxis, window.Apex.yaxis)), t.yaxis.constructor !== Array ? t.yaxis = [x.extend(i.yAxis, t.yaxis)] : t.yaxis = x.extendArray(t.yaxis, i.yAxis); var a = !1; t.yaxis.forEach((function (t) { t.logarithmic && (a = !0) })); var s = t.series; return e && !s && (s = e.config.series), a && s.length !== t.yaxis.length && s.length && (t.yaxis = s.map((function (e, a) { if (e.name || (s[a].name = "series-".concat(a + 1)), t.yaxis[a]) return t.yaxis[a].seriesName = s[a].name, t.yaxis[a]; var r = x.extend(i.yAxis, t.yaxis[0]); return r.show = !1, r }))), a && s.length > 1 && s.length !== t.yaxis.length && console.warn("A multi-series logarithmic chart should have equal number of series and y-axes"), t } }, { key: "extendAnnotations", value: function (t) { return void 0 === t.annotations && (t.annotations = {}, t.annotations.yaxis = [], t.annotations.xaxis = [], t.annotations.points = []), t = this.extendYAxisAnnotations(t), t = this.extendXAxisAnnotations(t), t = this.extendPointAnnotations(t) } }, { key: "extendYAxisAnnotations", value: function (t) { var e = new I; return t.annotations.yaxis = x.extendArray(void 0 !== t.annotations.yaxis ? t.annotations.yaxis : [], e.yAxisAnnotation), t } }, { key: "extendXAxisAnnotations", value: function (t) { var e = new I; return t.annotations.xaxis = x.extendArray(void 0 !== t.annotations.xaxis ? t.annotations.xaxis : [], e.xAxisAnnotation), t } }, { key: "extendPointAnnotations", value: function (t) { var e = new I; return t.annotations.points = x.extendArray(void 0 !== t.annotations.points ? t.annotations.points : [], e.pointAnnotation), t } }, { key: "checkForDarkTheme", value: function (t) { t.theme && "dark" === t.theme.mode && (t.tooltip || (t.tooltip = {}), "light" !== t.tooltip.theme && (t.tooltip.theme = "dark"), t.chart.foreColor || (t.chart.foreColor = "#f6f7f8"), t.chart.background || (t.chart.background = "#424242"), t.theme.palette || (t.theme.palette = "palette4")) } }, { key: "handleUserInputErrors", value: function (t) { var e = t; if (e.tooltip.shared && e.tooltip.intersect) throw new Error("tooltip.shared cannot be enabled when tooltip.intersect is true. Turn off any other option by setting it to false."); if ("bar" === e.chart.type && e.plotOptions.bar.horizontal) { if (e.yaxis.length > 1) throw new Error("Multiple Y Axis for bars are not supported. Switch to column chart by setting plotOptions.bar.horizontal=false"); e.yaxis[0].reversed && (e.yaxis[0].opposite = !0), e.xaxis.tooltip.enabled = !1, e.yaxis[0].tooltip.enabled = !1, e.chart.zoom.enabled = !1 } return "bar" !== e.chart.type && "rangeBar" !== e.chart.type || e.tooltip.shared && "barWidth" === e.xaxis.crosshairs.width && e.series.length > 1 && (e.xaxis.crosshairs.width = "tickWidth"), "candlestick" !== e.chart.type && "boxPlot" !== e.chart.type || e.yaxis[0].reversed && (console.warn("Reversed y-axis in ".concat(e.chart.type, " chart is not supported.")), e.yaxis[0].reversed = !1), e } }]), t }(), F = function () { function t() { a(this, t) } return r(t, [{ key: "initGlobalVars", value: function (t) { t.series = [], t.seriesCandleO = [], t.seriesCandleH = [], t.seriesCandleM = [], t.seriesCandleL = [], t.seriesCandleC = [], t.seriesRangeStart = [], t.seriesRangeEnd = [], t.seriesRange = [], t.seriesPercent = [], t.seriesGoals = [], t.seriesX = [], t.seriesZ = [], t.seriesNames = [], t.seriesTotals = [], t.seriesLog = [], t.seriesColors = [], t.stackedSeriesTotals = [], t.seriesXvalues = [], t.seriesYvalues = [], t.labels = [], t.hasXaxisGroups = !1, t.groups = [], t.barGroups = [], t.lineGroups = [], t.areaGroups = [], t.hasSeriesGroups = !1, t.seriesGroups = [], t.categoryLabels = [], t.timescaleLabels = [], t.noLabelsProvided = !1, t.resizeTimer = null, t.selectionResizeTimer = null, t.delayedElements = [], t.pointsArray = [], t.dataLabelsRects = [], t.isXNumeric = !1, t.skipLastTimelinelabel = !1, t.skipFirstTimelinelabel = !1, t.isDataXYZ = !1, t.isMultiLineX = !1, t.isMultipleYAxis = !1, t.maxY = -Number.MAX_VALUE, t.minY = Number.MIN_VALUE, t.minYArr = [], t.maxYArr = [], t.maxX = -Number.MAX_VALUE, t.minX = Number.MAX_VALUE, t.initialMaxX = -Number.MAX_VALUE, t.initialMinX = Number.MAX_VALUE, t.maxDate = 0, t.minDate = Number.MAX_VALUE, t.minZ = Number.MAX_VALUE, t.maxZ = -Number.MAX_VALUE, t.minXDiff = Number.MAX_VALUE, t.yAxisScale = [], t.xAxisScale = null, t.xAxisTicksPositions = [], t.yLabelsCoords = [], t.yTitleCoords = [], t.barPadForNumericAxis = 0, t.padHorizontal = 0, t.xRange = 0, t.yRange = [], t.zRange = 0, t.dataPoints = 0, t.xTickAmount = 0, t.multiAxisTickAmount = 0 } }, { key: "globalVars", value: function (t) { return { chartID: null, cuid: null, events: { beforeMount: [], mounted: [], updated: [], clicked: [], selection: [], dataPointSelection: [], zoomed: [], scrolled: [] }, colors: [], clientX: null, clientY: null, fill: { colors: [] }, stroke: { colors: [] }, dataLabels: { style: { colors: [] } }, radarPolygons: { fill: { colors: [] } }, markers: { colors: [], size: t.markers.size, largestSize: 0 }, animationEnded: !1, isTouchDevice: "ontouchstart" in window || navigator.msMaxTouchPoints, isDirty: !1, isExecCalled: !1, initialConfig: null, initialSeries: [], lastXAxis: [], lastYAxis: [], columnSeries: null, labels: [], timescaleLabels: [], noLabelsProvided: !1, allSeriesCollapsed: !1, collapsedSeries: [], collapsedSeriesIndices: [], ancillaryCollapsedSeries: [], ancillaryCollapsedSeriesIndices: [], risingSeries: [], dataFormatXNumeric: !1, capturedSeriesIndex: -1, capturedDataPointIndex: -1, selectedDataPoints: [], goldenPadding: 35, invalidLogScale: !1, ignoreYAxisIndexes: [], maxValsInArrayIndex: 0, radialSize: 0, selection: void 0, zoomEnabled: "zoom" === t.chart.toolbar.autoSelected && t.chart.toolbar.tools.zoom && t.chart.zoom.enabled, panEnabled: "pan" === t.chart.toolbar.autoSelected && t.chart.toolbar.tools.pan, selectionEnabled: "selection" === t.chart.toolbar.autoSelected && t.chart.toolbar.tools.selection, yaxis: null, mousedown: !1, lastClientPosition: {}, visibleXRange: void 0, yValueDecimal: 0, total: 0, SVGNS: "http://www.w3.org/2000/svg", svgWidth: 0, svgHeight: 0, noData: !1, locale: {}, dom: {}, memory: { methodsToExec: [] }, shouldAnimate: !0, skipLastTimelinelabel: !1, skipFirstTimelinelabel: !1, delayedElements: [], axisCharts: !0, isDataXYZ: !1, isSlopeChart: t.plotOptions.line.isSlopeChart, resized: !1, resizeTimer: null, comboCharts: !1, dataChanged: !1, previousPaths: [], allSeriesHasEqualX: !0, pointsArray: [], dataLabelsRects: [], lastDrawnDataLabelsIndexes: [], hasNullValues: !1, easing: null, zoomed: !1, gridWidth: 0, gridHeight: 0, rotateXLabels: !1, defaultLabels: !1, xLabelFormatter: void 0, yLabelFormatters: [], xaxisTooltipFormatter: void 0, ttKeyFormatter: void 0, ttVal: void 0, ttZFormatter: void 0, LINE_HEIGHT_RATIO: 1.618, xAxisLabelsHeight: 0, xAxisGroupLabelsHeight: 0, xAxisLabelsWidth: 0, yAxisLabelsWidth: 0, scaleX: 1, scaleY: 1, translateX: 0, translateY: 0, translateYAxisX: [], yAxisWidths: [], translateXAxisY: 0, translateXAxisX: 0, tooltip: null, niceScaleAllowedMagMsd: [[1, 1, 2, 5, 5, 5, 10, 10, 10, 10, 10], [1, 1, 2, 5, 5, 5, 10, 10, 10, 10, 10]], niceScaleDefaultTicks: [1, 2, 4, 4, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 12, 12, 12, 12, 12, 12, 12, 12, 12, 24], seriesYAxisMap: [], seriesYAxisReverseMap: [] } } }, { key: "init", value: function (t) { var e = this.globalVars(t); return this.initGlobalVars(e), e.initialConfig = x.extend({}, t), e.initialSeries = x.clone(t.series), e.lastXAxis = x.clone(e.initialConfig.xaxis), e.lastYAxis = x.clone(e.initialConfig.yaxis), e } }]), t }(), R = function () { function t(e) { a(this, t), this.opts = e } return r(t, [{ key: "init", value: function () { var t = new Y(this.opts).init({ responsiveOverride: !1 }); return { config: t, globals: (new F).init(t) } } }]), t }(), H = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w, this.opts = null, this.seriesIndex = 0 } return r(t, [{ key: "clippedImgArea", value: function (t) { var e = this.w, i = e.config, a = parseInt(e.globals.gridWidth, 10), s = parseInt(e.globals.gridHeight, 10), r = a > s ? a : s, o = t.image, n = 0, l = 0; void 0 === t.width && void 0 === t.height ? void 0 !== i.fill.image.width && void 0 !== i.fill.image.height ? (n = i.fill.image.width + 1, l = i.fill.image.height) : (n = r + 1, l = r) : (n = t.width, l = t.height); var h = document.createElementNS(e.globals.SVGNS, "pattern"); m.setAttrs(h, { id: t.patternID, patternUnits: t.patternUnits ? t.patternUnits : "userSpaceOnUse", width: n + "px", height: l + "px" }); var c = document.createElementNS(e.globals.SVGNS, "image"); h.appendChild(c), c.setAttributeNS(window.SVG.xlink, "href", o), m.setAttrs(c, { x: 0, y: 0, preserveAspectRatio: "none", width: n + "px", height: l + "px" }), c.style.opacity = t.opacity, e.globals.dom.elDefs.node.appendChild(h) } }, { key: "getSeriesIndex", value: function (t) { var e = this.w, i = e.config.chart.type; return ("bar" === i || "rangeBar" === i) && e.config.plotOptions.bar.distributed || "heatmap" === i || "treemap" === i ? this.seriesIndex = t.seriesNumber : this.seriesIndex = t.seriesNumber % e.globals.series.length, this.seriesIndex } }, { key: "fillPath", value: function (t) { var e = this.w; this.opts = t; var i, a, s, r = this.w.config; this.seriesIndex = this.getSeriesIndex(t); var o = this.getFillColors()[this.seriesIndex]; void 0 !== e.globals.seriesColors[this.seriesIndex] && (o = e.globals.seriesColors[this.seriesIndex]), "function" == typeof o && (o = o({ seriesIndex: this.seriesIndex, dataPointIndex: t.dataPointIndex, value: t.value, w: e })); var n = t.fillType ? t.fillType : this.getFillType(this.seriesIndex), l = Array.isArray(r.fill.opacity) ? r.fill.opacity[this.seriesIndex] : r.fill.opacity; t.color && (o = t.color), o || (o = "#fff", console.warn("undefined color - ApexCharts")); var h = o; if (-1 === o.indexOf("rgb") ? o.length < 9 && (h = x.hexToRgba(o, l)) : o.indexOf("rgba") > -1 && (l = x.getOpacityFromRGBA(o)), t.opacity && (l = t.opacity), "pattern" === n && (a = this.handlePatternFill({ fillConfig: t.fillConfig, patternFill: a, fillColor: o, fillOpacity: l, defaultColor: h })), "gradient" === n && (s = this.handleGradientFill({ fillConfig: t.fillConfig, fillColor: o, fillOpacity: l, i: this.seriesIndex })), "image" === n) { var c = r.fill.image.src, d = t.patternID ? t.patternID : ""; this.clippedImgArea({ opacity: l, image: Array.isArray(c) ? t.seriesNumber < c.length ? c[t.seriesNumber] : c[0] : c, width: t.width ? t.width : void 0, height: t.height ? t.height : void 0, patternUnits: t.patternUnits, patternID: "pattern".concat(e.globals.cuid).concat(t.seriesNumber + 1).concat(d) }), i = "url(#pattern".concat(e.globals.cuid).concat(t.seriesNumber + 1).concat(d, ")") } else i = "gradient" === n ? s : "pattern" === n ? a : h; return t.solid && (i = h), i } }, { key: "getFillType", value: function (t) { var e = this.w; return Array.isArray(e.config.fill.type) ? e.config.fill.type[t] : e.config.fill.type } }, { key: "getFillColors", value: function () { var t = this.w, e = t.config, i = this.opts, a = []; return t.globals.comboCharts ? "line" === t.config.series[this.seriesIndex].type ? Array.isArray(t.globals.stroke.colors) ? a = t.globals.stroke.colors : a.push(t.globals.stroke.colors) : Array.isArray(t.globals.fill.colors) ? a = t.globals.fill.colors : a.push(t.globals.fill.colors) : "line" === e.chart.type ? Array.isArray(t.globals.stroke.colors) ? a = t.globals.stroke.colors : a.push(t.globals.stroke.colors) : Array.isArray(t.globals.fill.colors) ? a = t.globals.fill.colors : a.push(t.globals.fill.colors), void 0 !== i.fillColors && (a = [], Array.isArray(i.fillColors) ? a = i.fillColors.slice() : a.push(i.fillColors)), a } }, { key: "handlePatternFill", value: function (t) { var e = t.fillConfig, i = t.patternFill, a = t.fillColor, s = t.fillOpacity, r = t.defaultColor, o = this.w.config.fill; e && (o = e); var n = this.opts, l = new m(this.ctx), h = Array.isArray(o.pattern.strokeWidth) ? o.pattern.strokeWidth[this.seriesIndex] : o.pattern.strokeWidth, c = a; Array.isArray(o.pattern.style) ? i = void 0 !== o.pattern.style[n.seriesNumber] ? l.drawPattern(o.pattern.style[n.seriesNumber], o.pattern.width, o.pattern.height, c, h, s) : r : i = l.drawPattern(o.pattern.style, o.pattern.width, o.pattern.height, c, h, s); return i } }, { key: "handleGradientFill", value: function (t) { var i = t.fillColor, a = t.fillOpacity, s = t.fillConfig, r = t.i, o = this.w.config.fill; s && (o = e(e({}, o), s)); var n, l = this.opts, h = new m(this.ctx), c = new x, d = o.gradient.type, g = i, u = void 0 === o.gradient.opacityFrom ? a : Array.isArray(o.gradient.opacityFrom) ? o.gradient.opacityFrom[r] : o.gradient.opacityFrom; g.indexOf("rgba") > -1 && (u = x.getOpacityFromRGBA(g)); var p = void 0 === o.gradient.opacityTo ? a : Array.isArray(o.gradient.opacityTo) ? o.gradient.opacityTo[r] : o.gradient.opacityTo; if (void 0 === o.gradient.gradientToColors || 0 === o.gradient.gradientToColors.length) n = "dark" === o.gradient.shade ? c.shadeColor(-1 * parseFloat(o.gradient.shadeIntensity), i.indexOf("rgb") > -1 ? x.rgb2hex(i) : i) : c.shadeColor(parseFloat(o.gradient.shadeIntensity), i.indexOf("rgb") > -1 ? x.rgb2hex(i) : i); else if (o.gradient.gradientToColors[l.seriesNumber]) { var f = o.gradient.gradientToColors[l.seriesNumber]; n = f, f.indexOf("rgba") > -1 && (p = x.getOpacityFromRGBA(f)) } else n = i; if (o.gradient.gradientFrom && (g = o.gradient.gradientFrom), o.gradient.gradientTo && (n = o.gradient.gradientTo), o.gradient.inverseColors) { var b = g; g = n, n = b } return g.indexOf("rgb") > -1 && (g = x.rgb2hex(g)), n.indexOf("rgb") > -1 && (n = x.rgb2hex(n)), h.drawGradient(d, g, n, u, p, l.size, o.gradient.stops, o.gradient.colorStops, r) } }]), t }(), D = function () { function t(e, i) { a(this, t), this.ctx = e, this.w = e.w } return r(t, [{ key: "setGlobalMarkerSize", value: function () { var t = this.w; if (t.globals.markers.size = Array.isArray(t.config.markers.size) ? t.config.markers.size : [t.config.markers.size], t.globals.markers.size.length > 0) { if (t.globals.markers.size.length < t.globals.series.length + 1) for (var e = 0; e <= t.globals.series.length; e++)void 0 === t.globals.markers.size[e] && t.globals.markers.size.push(t.globals.markers.size[0]) } else t.globals.markers.size = t.config.series.map((function (e) { return t.config.markers.size })) } }, { key: "plotChartMarkers", value: function (t, e, i, a) { var s, r = arguments.length > 4 && void 0 !== arguments[4] && arguments[4], o = this.w, n = e, l = t, h = null, c = new m(this.ctx), d = o.config.markers.discrete && o.config.markers.discrete.length; if ((o.globals.markers.size[e] > 0 || r || d) && (h = c.group({ class: r || d ? "" : "apexcharts-series-markers" })).attr("clip-path", "url(#gridRectMarkerMask".concat(o.globals.cuid, ")")), Array.isArray(l.x)) for (var g = 0; g < l.x.length; g++) { var u = i; 1 === i && 0 === g && (u = 0), 1 === i && 1 === g && (u = 1); var p = "apexcharts-marker"; if ("line" !== o.config.chart.type && "area" !== o.config.chart.type || o.globals.comboCharts || o.config.tooltip.intersect || (p += " no-pointer-events"), (Array.isArray(o.config.markers.size) ? o.globals.markers.size[e] > 0 : o.config.markers.size > 0) || r || d) { x.isNumber(l.y[g]) ? p += " w".concat(x.randomId()) : p = "apexcharts-nullpoint"; var f = this.getMarkerConfig({ cssClass: p, seriesIndex: e, dataPointIndex: u }); o.config.series[n].data[u] && (o.config.series[n].data[u].fillColor && (f.pointFillColor = o.config.series[n].data[u].fillColor), o.config.series[n].data[u].strokeColor && (f.pointStrokeColor = o.config.series[n].data[u].strokeColor)), a && (f.pSize = a), (l.x[g] < -o.globals.markers.largestSize || l.x[g] > o.globals.gridWidth + o.globals.markers.largestSize || l.y[g] < -o.globals.markers.largestSize || l.y[g] > o.globals.gridHeight + o.globals.markers.largestSize) && (f.pSize = 0), (s = c.drawMarker(l.x[g], l.y[g], f)).attr("rel", u), s.attr("j", u), s.attr("index", e), s.node.setAttribute("default-marker-size", f.pSize), new v(this.ctx).setSelectionFilter(s, e, u), this.addEvents(s), h && h.add(s) } else void 0 === o.globals.pointsArray[e] && (o.globals.pointsArray[e] = []), o.globals.pointsArray[e].push([l.x[g], l.y[g]]) } return h } }, { key: "getMarkerConfig", value: function (t) { var e = t.cssClass, i = t.seriesIndex, a = t.dataPointIndex, s = void 0 === a ? null : a, r = t.finishRadius, o = void 0 === r ? null : r, n = this.w, l = this.getMarkerStyle(i), h = n.globals.markers.size[i], c = n.config.markers; return null !== s && c.discrete.length && c.discrete.map((function (t) { t.seriesIndex === i && t.dataPointIndex === s && (l.pointStrokeColor = t.strokeColor, l.pointFillColor = t.fillColor, h = t.size, l.pointShape = t.shape) })), { pSize: null === o ? h : o, pRadius: c.radius, width: Array.isArray(c.width) ? c.width[i] : c.width, height: Array.isArray(c.height) ? c.height[i] : c.height, pointStrokeWidth: Array.isArray(c.strokeWidth) ? c.strokeWidth[i] : c.strokeWidth, pointStrokeColor: l.pointStrokeColor, pointFillColor: l.pointFillColor, shape: l.pointShape || (Array.isArray(c.shape) ? c.shape[i] : c.shape), class: e, pointStrokeOpacity: Array.isArray(c.strokeOpacity) ? c.strokeOpacity[i] : c.strokeOpacity, pointStrokeDashArray: Array.isArray(c.strokeDashArray) ? c.strokeDashArray[i] : c.strokeDashArray, pointFillOpacity: Array.isArray(c.fillOpacity) ? c.fillOpacity[i] : c.fillOpacity, seriesIndex: i } } }, { key: "addEvents", value: function (t) { var e = this.w, i = new m(this.ctx); t.node.addEventListener("mouseenter", i.pathMouseEnter.bind(this.ctx, t)), t.node.addEventListener("mouseleave", i.pathMouseLeave.bind(this.ctx, t)), t.node.addEventListener("mousedown", i.pathMouseDown.bind(this.ctx, t)), t.node.addEventListener("click", e.config.markers.onClick), t.node.addEventListener("dblclick", e.config.markers.onDblClick), t.node.addEventListener("touchstart", i.pathMouseDown.bind(this.ctx, t), { passive: !0 }) } }, { key: "getMarkerStyle", value: function (t) { var e = this.w, i = e.globals.markers.colors, a = e.config.markers.strokeColor || e.config.markers.strokeColors; return { pointStrokeColor: Array.isArray(a) ? a[t] : a, pointFillColor: Array.isArray(i) ? i[t] : i } } }]), t }(), O = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w, this.initialAnim = this.w.config.chart.animations.enabled, this.dynamicAnim = this.initialAnim && this.w.config.chart.animations.dynamicAnimation.enabled } return r(t, [{ key: "draw", value: function (t, e, i) { var a = this.w, s = new m(this.ctx), r = i.realIndex, o = i.pointsPos, n = i.zRatio, l = i.elParent, h = s.group({ class: "apexcharts-series-markers apexcharts-series-".concat(a.config.chart.type) }); if (h.attr("clip-path", "url(#gridRectMarkerMask".concat(a.globals.cuid, ")")), Array.isArray(o.x)) for (var c = 0; c < o.x.length; c++) { var d = e + 1, g = !0; 0 === e && 0 === c && (d = 0), 0 === e && 1 === c && (d = 1); var u = 0, p = a.globals.markers.size[r]; if (n !== 1 / 0) { var f = a.config.plotOptions.bubble; p = a.globals.seriesZ[r][d], f.zScaling && (p /= n), f.minBubbleRadius && p < f.minBubbleRadius && (p = f.minBubbleRadius), f.maxBubbleRadius && p > f.maxBubbleRadius && (p = f.maxBubbleRadius) } a.config.chart.animations.enabled || (u = p); var x = o.x[c], b = o.y[c]; if (u = u || 0, null !== b && void 0 !== a.globals.series[r][d] || (g = !1), g) { var v = this.drawPoint(x, b, u, p, r, d, e); h.add(v) } l.add(h) } } }, { key: "drawPoint", value: function (t, e, i, a, s, r, o) { var n = this.w, l = s, h = new b(this.ctx), c = new v(this.ctx), d = new H(this.ctx), g = new D(this.ctx), u = new m(this.ctx), p = g.getMarkerConfig({ cssClass: "apexcharts-marker", seriesIndex: l, dataPointIndex: r, finishRadius: "bubble" === n.config.chart.type || n.globals.comboCharts && n.config.series[s] && "bubble" === n.config.series[s].type ? a : null }); a = p.pSize; var f, x = d.fillPath({ seriesNumber: s, dataPointIndex: r, color: p.pointFillColor, patternUnits: "objectBoundingBox", value: n.globals.series[s][o] }); if ("circle" === p.shape ? f = u.drawCircle(i) : "square" !== p.shape && "rect" !== p.shape || (f = u.drawRect(0, 0, p.width - p.pointStrokeWidth / 2, p.height - p.pointStrokeWidth / 2, p.pRadius)), n.config.series[l].data[r] && n.config.series[l].data[r].fillColor && (x = n.config.series[l].data[r].fillColor), f.attr({ x: t - p.width / 2 - p.pointStrokeWidth / 2, y: e - p.height / 2 - p.pointStrokeWidth / 2, cx: t, cy: e, fill: x, "fill-opacity": p.pointFillOpacity, stroke: p.pointStrokeColor, r: a, "stroke-width": p.pointStrokeWidth, "stroke-dasharray": p.pointStrokeDashArray, "stroke-opacity": p.pointStrokeOpacity }), n.config.chart.dropShadow.enabled) { var y = n.config.chart.dropShadow; c.dropShadow(f, y, s) } if (!this.initialAnim || n.globals.dataChanged || n.globals.resized) n.globals.animationEnded = !0; else { var w = n.config.chart.animations.speed; h.animateMarker(f, 0, "circle" === p.shape ? a : { width: p.width, height: p.height }, w, n.globals.easing, (function () { window.setTimeout((function () { h.animationCompleted(f) }), 100) })) } if (n.globals.dataChanged && "circle" === p.shape) if (this.dynamicAnim) { var k, A, S, C, L = n.config.chart.animations.dynamicAnimation.speed; null != (C = n.globals.previousPaths[s] && n.globals.previousPaths[s][o]) && (k = C.x, A = C.y, S = void 0 !== C.r ? C.r : a); for (var P = 0; P < n.globals.collapsedSeries.length; P++)n.globals.collapsedSeries[P].index === s && (L = 1, a = 0); 0 === t && 0 === e && (a = 0), h.animateCircle(f, { cx: k, cy: A, r: S }, { cx: t, cy: e, r: a }, L, n.globals.easing) } else f.attr({ r: a }); return f.attr({ rel: r, j: r, index: s, "default-marker-size": a }), c.setSelectionFilter(f, s, r), g.addEvents(f), f.node.classList.add("apexcharts-marker"), f } }, { key: "centerTextInBubble", value: function (t) { var e = this.w; return { y: t += parseInt(e.config.dataLabels.style.fontSize, 10) / 4 } } }]), t }(), N = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w } return r(t, [{ key: "dataLabelsCorrection", value: function (t, e, i, a, s, r, o) { var n = this.w, l = !1, h = new m(this.ctx).getTextRects(i, o), c = h.width, d = h.height; e < 0 && (e = 0), e > n.globals.gridHeight + d && (e = n.globals.gridHeight + d / 2), void 0 === n.globals.dataLabelsRects[a] && (n.globals.dataLabelsRects[a] = []), n.globals.dataLabelsRects[a].push({ x: t, y: e, width: c, height: d }); var g = n.globals.dataLabelsRects[a].length - 2, u = void 0 !== n.globals.lastDrawnDataLabelsIndexes[a] ? n.globals.lastDrawnDataLabelsIndexes[a][n.globals.lastDrawnDataLabelsIndexes[a].length - 1] : 0; if (void 0 !== n.globals.dataLabelsRects[a][g]) { var p = n.globals.dataLabelsRects[a][u]; (t > p.x + p.width || e > p.y + p.height || e + d < p.y || t + c < p.x) && (l = !0) } return (0 === s || r) && (l = !0), { x: t, y: e, textRects: h, drawnextLabel: l } } }, { key: "drawDataLabel", value: function (t) { var e = this, i = t.type, a = t.pos, s = t.i, r = t.j, o = t.isRangeStart, n = t.strokeWidth, l = void 0 === n ? 2 : n, h = this.w, c = new m(this.ctx), d = h.config.dataLabels, g = 0, u = 0, p = r, f = null; if (-1 !== h.globals.collapsedSeriesIndices.indexOf(s) || !d.enabled || !Array.isArray(a.x)) return f; f = c.group({ class: "apexcharts-data-labels" }); for (var x = 0; x < a.x.length; x++)if (g = a.x[x] + d.offsetX, u = a.y[x] + d.offsetY + l, !isNaN(g)) { 1 === r && 0 === x && (p = 0), 1 === r && 1 === x && (p = 1); var b = h.globals.series[s][p]; "rangeArea" === i && (b = o ? h.globals.seriesRangeStart[s][p] : h.globals.seriesRangeEnd[s][p]); var v = "", y = function (t) { return h.config.dataLabels.formatter(t, { ctx: e.ctx, seriesIndex: s, dataPointIndex: p, w: h }) }; if ("bubble" === h.config.chart.type) v = y(b = h.globals.seriesZ[s][p]), u = a.y[x], u = new O(this.ctx).centerTextInBubble(u, s, p).y; else void 0 !== b && (v = y(b)); var w = h.config.dataLabels.textAnchor; h.globals.isSlopeChart && (w = 0 === p ? "end" : p === h.config.series[s].data.length - 1 ? "start" : "middle"), this.plotDataLabelsText({ x: g, y: u, text: v, i: s, j: p, parent: f, offsetCorrection: !0, dataLabelsConfig: h.config.dataLabels, textAnchor: w }) } return f } }, { key: "plotDataLabelsText", value: function (t) { var e = this.w, i = new m(this.ctx), a = t.x, s = t.y, r = t.i, o = t.j, n = t.text, l = t.textAnchor, h = t.fontSize, c = t.parent, d = t.dataLabelsConfig, g = t.color, u = t.alwaysDrawDataLabel, p = t.offsetCorrection; if (!(Array.isArray(e.config.dataLabels.enabledOnSeries) && e.config.dataLabels.enabledOnSeries.indexOf(r) < 0)) { var f = { x: a, y: s, drawnextLabel: !0, textRects: null }; p && (f = this.dataLabelsCorrection(a, s, n, r, o, u, parseInt(d.style.fontSize, 10))), e.globals.zoomed || (a = f.x, s = f.y), f.textRects && (a < -20 - f.textRects.width || a > e.globals.gridWidth + f.textRects.width + 30) && (n = ""); var x = e.globals.dataLabels.style.colors[r]; (("bar" === e.config.chart.type || "rangeBar" === e.config.chart.type) && e.config.plotOptions.bar.distributed || e.config.dataLabels.distributed) && (x = e.globals.dataLabels.style.colors[o]), "function" == typeof x && (x = x({ series: e.globals.series, seriesIndex: r, dataPointIndex: o, w: e })), g && (x = g); var b = d.offsetX, y = d.offsetY; if ("bar" !== e.config.chart.type && "rangeBar" !== e.config.chart.type || (b = 0, y = 0), e.globals.isSlopeChart && (0 !== o && (b = -2 * d.offsetX + 5), 0 !== o && o !== e.config.series[r].data.length - 1 && (b = 0)), f.drawnextLabel) { var w = i.drawText({ width: 100, height: parseInt(d.style.fontSize, 10), x: a + b, y: s + y, foreColor: x, textAnchor: l || d.textAnchor, text: n, fontSize: h || d.style.fontSize, fontFamily: d.style.fontFamily, fontWeight: d.style.fontWeight || "normal" }); if (w.attr({ class: "apexcharts-datalabel", cx: a, cy: s }), d.dropShadow.enabled) { var k = d.dropShadow; new v(this.ctx).dropShadow(w, k) } c.add(w), void 0 === e.globals.lastDrawnDataLabelsIndexes[r] && (e.globals.lastDrawnDataLabelsIndexes[r] = []), e.globals.lastDrawnDataLabelsIndexes[r].push(o) } } } }, { key: "addBackgroundToDataLabel", value: function (t, e) { var i = this.w, a = i.config.dataLabels.background, s = a.padding, r = a.padding / 2, o = e.width, n = e.height, l = new m(this.ctx).drawRect(e.x - s, e.y - r / 2, o + 2 * s, n + r, a.borderRadius, "transparent" === i.config.chart.background ? "#fff" : i.config.chart.background, a.opacity, a.borderWidth, a.borderColor); a.dropShadow.enabled && new v(this.ctx).dropShadow(l, a.dropShadow); return l } }, { key: "dataLabelsBackground", value: function () { var t = this.w; if ("bubble" !== t.config.chart.type) for (var e = t.globals.dom.baseEl.querySelectorAll(".apexcharts-datalabels text"), i = 0; i < e.length; i++) { var a = e[i], s = a.getBBox(), r = null; if (s.width && s.height && (r = this.addBackgroundToDataLabel(a, s)), r) { a.parentNode.insertBefore(r.node, a); var o = a.getAttribute("fill"); t.config.chart.animations.enabled && !t.globals.resized && !t.globals.dataChanged ? r.animate().attr({ fill: o }) : r.attr({ fill: o }), a.setAttribute("fill", t.config.dataLabels.background.foreColor) } } } }, { key: "bringForward", value: function () { for (var t = this.w, e = t.globals.dom.baseEl.querySelectorAll(".apexcharts-datalabels"), i = t.globals.dom.baseEl.querySelector(".apexcharts-plot-series:last-child"), a = 0; a < e.length; a++)i && i.insertBefore(e[a], i.nextSibling) } }]), t }(), W = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w, this.legendInactiveClass = "legend-mouseover-inactive" } return r(t, [{ key: "getAllSeriesEls", value: function () { return this.w.globals.dom.baseEl.getElementsByClassName("apexcharts-series") } }, { key: "getSeriesByName", value: function (t) { return this.w.globals.dom.baseEl.querySelector(".apexcharts-inner .apexcharts-series[seriesName='".concat(x.escapeString(t), "']")) } }, { key: "isSeriesHidden", value: function (t) { var e = this.getSeriesByName(t), i = parseInt(e.getAttribute("data:realIndex"), 10); return { isHidden: e.classList.contains("apexcharts-series-collapsed"), realIndex: i } } }, { key: "addCollapsedClassToSeries", value: function (t, e) { var i = this.w; function a(i) { for (var a = 0; a < i.length; a++)i[a].index === e && t.node.classList.add("apexcharts-series-collapsed") } a(i.globals.collapsedSeries), a(i.globals.ancillaryCollapsedSeries) } }, { key: "toggleSeries", value: function (t) { var e = this.isSeriesHidden(t); return this.ctx.legend.legendHelpers.toggleDataSeries(e.realIndex, e.isHidden), e.isHidden } }, { key: "showSeries", value: function (t) { var e = this.isSeriesHidden(t); e.isHidden && this.ctx.legend.legendHelpers.toggleDataSeries(e.realIndex, !0) } }, { key: "hideSeries", value: function (t) { var e = this.isSeriesHidden(t); e.isHidden || this.ctx.legend.legendHelpers.toggleDataSeries(e.realIndex, !1) } }, { key: "resetSeries", value: function () { var t = !(arguments.length > 0 && void 0 !== arguments[0]) || arguments[0], e = !(arguments.length > 1 && void 0 !== arguments[1]) || arguments[1], i = !(arguments.length > 2 && void 0 !== arguments[2]) || arguments[2], a = this.w, s = x.clone(a.globals.initialSeries); a.globals.previousPaths = [], i ? (a.globals.collapsedSeries = [], a.globals.ancillaryCollapsedSeries = [], a.globals.collapsedSeriesIndices = [], a.globals.ancillaryCollapsedSeriesIndices = []) : s = this.emptyCollapsedSeries(s), a.config.series = s, t && (e && (a.globals.zoomed = !1, this.ctx.updateHelpers.revertDefaultAxisMinMax()), this.ctx.updateHelpers._updateSeries(s, a.config.chart.animations.dynamicAnimation.enabled)) } }, { key: "emptyCollapsedSeries", value: function (t) { for (var e = this.w, i = 0; i < t.length; i++)e.globals.collapsedSeriesIndices.indexOf(i) > -1 && (t[i].data = []); return t } }, { key: "toggleSeriesOnHover", value: function (t, e) { var i = this.w; e || (e = t.target); var a = i.globals.dom.baseEl.querySelectorAll(".apexcharts-series, .apexcharts-datalabels, .apexcharts-yaxis"); if ("mousemove" === t.type) { var s = parseInt(e.getAttribute("rel"), 10) - 1, r = null, o = null, n = null; if (i.globals.axisCharts || "radialBar" === i.config.chart.type) if (i.globals.axisCharts) { r = i.globals.dom.baseEl.querySelector(".apexcharts-series[data\\:realIndex='".concat(s, "']")), o = i.globals.dom.baseEl.querySelector(".apexcharts-datalabels[data\\:realIndex='".concat(s, "']")); var l = i.globals.seriesYAxisReverseMap[s]; n = i.globals.dom.baseEl.querySelector(".apexcharts-yaxis[rel='".concat(l, "']")) } else r = i.globals.dom.baseEl.querySelector(".apexcharts-series[rel='".concat(s + 1, "']")); else r = i.globals.dom.baseEl.querySelector(".apexcharts-series[rel='".concat(s + 1, "'] path")); for (var h = 0; h < a.length; h++)a[h].classList.add(this.legendInactiveClass); null !== r && (i.globals.axisCharts || r.parentNode.classList.remove(this.legendInactiveClass), r.classList.remove(this.legendInactiveClass), null !== o && o.classList.remove(this.legendInactiveClass), null !== n && n.classList.remove(this.legendInactiveClass)) } else if ("mouseout" === t.type) for (var c = 0; c < a.length; c++)a[c].classList.remove(this.legendInactiveClass) } }, { key: "highlightRangeInSeries", value: function (t, e) { var i = this, a = this.w, s = a.globals.dom.baseEl.getElementsByClassName("apexcharts-heatmap-rect"), r = function (t) { for (var e = 0; e < s.length; e++)s[e].classList[t](i.legendInactiveClass) }; if ("mousemove" === t.type) { var o = parseInt(e.getAttribute("rel"), 10) - 1; r("add"), function (t) { for (var e = 0; e < s.length; e++) { var a = parseInt(s[e].getAttribute("val"), 10); a >= t.from && a <= t.to && s[e].classList.remove(i.legendInactiveClass) } }(a.config.plotOptions.heatmap.colorScale.ranges[o]) } else "mouseout" === t.type && r("remove") } }, { key: "getActiveConfigSeriesIndex", value: function () { var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "asc", e = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : [], i = this.w, a = 0; if (i.config.series.length > 1) for (var s = i.config.series.map((function (t, a) { return t.data && t.data.length > 0 && -1 === i.globals.collapsedSeriesIndices.indexOf(a) && (!i.globals.comboCharts || 0 === e.length || e.length && e.indexOf(i.config.series[a].type) > -1) ? a : -1 })), r = "asc" === t ? 0 : s.length - 1; "asc" === t ? r < s.length : r >= 0; "asc" === t ? r++ : r--)if (-1 !== s[r]) { a = s[r]; break } return a } }, { key: "getBarSeriesIndices", value: function () { return this.w.globals.comboCharts ? this.w.config.series.map((function (t, e) { return "bar" === t.type || "column" === t.type ? e : -1 })).filter((function (t) { return -1 !== t })) : this.w.config.series.map((function (t, e) { return e })) } }, { key: "getPreviousPaths", value: function () { var t = this.w; function e(e, i, a) { for (var s = e[i].childNodes, r = { type: a, paths: [], realIndex: e[i].getAttribute("data:realIndex") }, o = 0; o < s.length; o++)if (s[o].hasAttribute("pathTo")) { var n = s[o].getAttribute("pathTo"); r.paths.push({ d: n }) } t.globals.previousPaths.push(r) } t.globals.previousPaths = [];["line", "area", "bar", "rangebar", "rangeArea", "candlestick", "radar"].forEach((function (i) { for (var a, s = (a = i, t.globals.dom.baseEl.querySelectorAll(".apexcharts-".concat(a, "-series .apexcharts-series"))), r = 0; r < s.length; r++)e(s, r, i) })), this.handlePrevBubbleScatterPaths("bubble"), this.handlePrevBubbleScatterPaths("scatter"); var i = t.globals.dom.baseEl.querySelectorAll(".apexcharts-".concat(t.config.chart.type, " .apexcharts-series")); if (i.length > 0) for (var a = function (e) { for (var i = t.globals.dom.baseEl.querySelectorAll(".apexcharts-".concat(t.config.chart.type, " .apexcharts-series[data\\:realIndex='").concat(e, "'] rect")), a = [], s = function (t) { var e = function (e) { return i[t].getAttribute(e) }, s = { x: parseFloat(e("x")), y: parseFloat(e("y")), width: parseFloat(e("width")), height: parseFloat(e("height")) }; a.push({ rect: s, color: i[t].getAttribute("color") }) }, r = 0; r < i.length; r++)s(r); t.globals.previousPaths.push(a) }, s = 0; s < i.length; s++)a(s); t.globals.axisCharts || (t.globals.previousPaths = t.globals.series) } }, { key: "handlePrevBubbleScatterPaths", value: function (t) { var e = this.w, i = e.globals.dom.baseEl.querySelectorAll(".apexcharts-".concat(t, "-series .apexcharts-series")); if (i.length > 0) for (var a = 0; a < i.length; a++) { for (var s = e.globals.dom.baseEl.querySelectorAll(".apexcharts-".concat(t, "-series .apexcharts-series[data\\:realIndex='").concat(a, "'] circle")), r = [], o = 0; o < s.length; o++)r.push({ x: s[o].getAttribute("cx"), y: s[o].getAttribute("cy"), r: s[o].getAttribute("r") }); e.globals.previousPaths.push(r) } } }, { key: "clearPreviousPaths", value: function () { var t = this.w; t.globals.previousPaths = [], t.globals.allSeriesCollapsed = !1 } }, { key: "handleNoData", value: function () { var t = this.w, e = t.config.noData, i = new m(this.ctx), a = t.globals.svgWidth / 2, s = t.globals.svgHeight / 2, r = "middle"; if (t.globals.noData = !0, t.globals.animationEnded = !0, "left" === e.align ? (a = 10, r = "start") : "right" === e.align && (a = t.globals.svgWidth - 10, r = "end"), "top" === e.verticalAlign ? s = 50 : "bottom" === e.verticalAlign && (s = t.globals.svgHeight - 50), a += e.offsetX, s = s + parseInt(e.style.fontSize, 10) + 2 + e.offsetY, void 0 !== e.text && "" !== e.text) { var o = i.drawText({ x: a, y: s, text: e.text, textAnchor: r, fontSize: e.style.fontSize, fontFamily: e.style.fontFamily, foreColor: e.style.color, opacity: 1, class: "apexcharts-text-nodata" }); t.globals.dom.Paper.add(o) } } }, { key: "setNullSeriesToZeroValues", value: function (t) { for (var e = this.w, i = 0; i < t.length; i++)if (0 === t[i].length) for (var a = 0; a < t[e.globals.maxValsInArrayIndex].length; a++)t[i].push(0); return t } }, { key: "hasAllSeriesEqualX", value: function () { for (var t = !0, e = this.w, i = this.filteredSeriesX(), a = 0; a < i.length - 1; a++)if (i[a][0] !== i[a + 1][0]) { t = !1; break } return e.globals.allSeriesHasEqualX = t, t } }, { key: "filteredSeriesX", value: function () { var t = this.w.globals.seriesX.map((function (t) { return t.length > 0 ? t : [] })); return t } }]), t }(), B = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w, this.twoDSeries = [], this.threeDSeries = [], this.twoDSeriesX = [], this.seriesGoals = [], this.coreUtils = new y(this.ctx) } return r(t, [{ key: "isMultiFormat", value: function () { return this.isFormatXY() || this.isFormat2DArray() } }, { key: "isFormatXY", value: function () { var t = this.w.config.series.slice(), e = new W(this.ctx); if (this.activeSeriesIndex = e.getActiveConfigSeriesIndex(), void 0 !== t[this.activeSeriesIndex].data && t[this.activeSeriesIndex].data.length > 0 && null !== t[this.activeSeriesIndex].data[0] && void 0 !== t[this.activeSeriesIndex].data[0].x && null !== t[this.activeSeriesIndex].data[0]) return !0 } }, { key: "isFormat2DArray", value: function () { var t = this.w.config.series.slice(), e = new W(this.ctx); if (this.activeSeriesIndex = e.getActiveConfigSeriesIndex(), void 0 !== t[this.activeSeriesIndex].data && t[this.activeSeriesIndex].data.length > 0 && void 0 !== t[this.activeSeriesIndex].data[0] && null !== t[this.activeSeriesIndex].data[0] && t[this.activeSeriesIndex].data[0].constructor === Array) return !0 } }, { key: "handleFormat2DArray", value: function (t, e) { for (var i = this.w.config, a = this.w.globals, s = "boxPlot" === i.chart.type || "boxPlot" === i.series[e].type, r = 0; r < t[e].data.length; r++)if (void 0 !== t[e].data[r][1] && (Array.isArray(t[e].data[r][1]) && 4 === t[e].data[r][1].length && !s ? this.twoDSeries.push(x.parseNumber(t[e].data[r][1][3])) : t[e].data[r].length >= 5 ? this.twoDSeries.push(x.parseNumber(t[e].data[r][4])) : this.twoDSeries.push(x.parseNumber(t[e].data[r][1])), a.dataFormatXNumeric = !0), "datetime" === i.xaxis.type) { var o = new Date(t[e].data[r][0]); o = new Date(o).getTime(), this.twoDSeriesX.push(o) } else this.twoDSeriesX.push(t[e].data[r][0]); for (var n = 0; n < t[e].data.length; n++)void 0 !== t[e].data[n][2] && (this.threeDSeries.push(t[e].data[n][2]), a.isDataXYZ = !0) } }, { key: "handleFormatXY", value: function (t, e) { var i = this.w.config, a = this.w.globals, s = new A(this.ctx), r = e; a.collapsedSeriesIndices.indexOf(e) > -1 && (r = this.activeSeriesIndex); for (var o = 0; o < t[e].data.length; o++)void 0 !== t[e].data[o].y && (Array.isArray(t[e].data[o].y) ? this.twoDSeries.push(x.parseNumber(t[e].data[o].y[t[e].data[o].y.length - 1])) : this.twoDSeries.push(x.parseNumber(t[e].data[o].y))), void 0 !== t[e].data[o].goals && Array.isArray(t[e].data[o].goals) ? (void 0 === this.seriesGoals[e] && (this.seriesGoals[e] = []), this.seriesGoals[e].push(t[e].data[o].goals)) : (void 0 === this.seriesGoals[e] && (this.seriesGoals[e] = []), this.seriesGoals[e].push(null)); for (var n = 0; n < t[r].data.length; n++) { var l = "string" == typeof t[r].data[n].x, h = Array.isArray(t[r].data[n].x), c = !h && !!s.isValidDate(t[r].data[n].x); if (l || c) if (l || i.xaxis.convertedCatToNumeric) { var d = a.isBarHorizontal && a.isRangeData; "datetime" !== i.xaxis.type || d ? (this.fallbackToCategory = !0, this.twoDSeriesX.push(t[r].data[n].x), isNaN(t[r].data[n].x) || "category" === this.w.config.xaxis.type || "string" == typeof t[r].data[n].x || (a.isXNumeric = !0)) : this.twoDSeriesX.push(s.parseDate(t[r].data[n].x)) } else "datetime" === i.xaxis.type ? this.twoDSeriesX.push(s.parseDate(t[r].data[n].x.toString())) : (a.dataFormatXNumeric = !0, a.isXNumeric = !0, this.twoDSeriesX.push(parseFloat(t[r].data[n].x))); else h ? (this.fallbackToCategory = !0, this.twoDSeriesX.push(t[r].data[n].x)) : (a.isXNumeric = !0, a.dataFormatXNumeric = !0, this.twoDSeriesX.push(t[r].data[n].x)) } if (t[e].data[0] && void 0 !== t[e].data[0].z) { for (var g = 0; g < t[e].data.length; g++)this.threeDSeries.push(t[e].data[g].z); a.isDataXYZ = !0 } } }, { key: "handleRangeData", value: function (t, e) { var i = this.w.globals, a = {}; return this.isFormat2DArray() ? a = this.handleRangeDataFormat("array", t, e) : this.isFormatXY() && (a = this.handleRangeDataFormat("xy", t, e)), i.seriesRangeStart.push(void 0 === a.start ? [] : a.start), i.seriesRangeEnd.push(void 0 === a.end ? [] : a.end), i.seriesRange.push(a.rangeUniques), i.seriesRange.forEach((function (t, e) { t && t.forEach((function (t, e) { t.y.forEach((function (e, i) { for (var a = 0; a < t.y.length; a++)if (i !== a) { var s = e.y1, r = e.y2, o = t.y[a].y1; s <= t.y[a].y2 && o <= r && (t.overlaps.indexOf(e.rangeName) < 0 && t.overlaps.push(e.rangeName), t.overlaps.indexOf(t.y[a].rangeName) < 0 && t.overlaps.push(t.y[a].rangeName)) } })) })) })), a } }, { key: "handleCandleStickBoxData", value: function (t, e) { var i = this.w.globals, a = {}; return this.isFormat2DArray() ? a = this.handleCandleStickBoxDataFormat("array", t, e) : this.isFormatXY() && (a = this.handleCandleStickBoxDataFormat("xy", t, e)), i.seriesCandleO[e] = a.o, i.seriesCandleH[e] = a.h, i.seriesCandleM[e] = a.m, i.seriesCandleL[e] = a.l, i.seriesCandleC[e] = a.c, a } }, { key: "handleRangeDataFormat", value: function (t, e, i) { var a = [], s = [], r = e[i].data.filter((function (t, e, i) { return e === i.findIndex((function (e) { return e.x === t.x })) })).map((function (t, e) { return { x: t.x, overlaps: [], y: [] } })); if ("array" === t) for (var o = 0; o < e[i].data.length; o++)Array.isArray(e[i].data[o]) ? (a.push(e[i].data[o][1][0]), s.push(e[i].data[o][1][1])) : (a.push(e[i].data[o]), s.push(e[i].data[o])); else if ("xy" === t) for (var n = function (t) { var o = Array.isArray(e[i].data[t].y), n = x.randomId(), l = e[i].data[t].x, h = { y1: o ? e[i].data[t].y[0] : e[i].data[t].y, y2: o ? e[i].data[t].y[1] : e[i].data[t].y, rangeName: n }; e[i].data[t].rangeName = n; var c = r.findIndex((function (t) { return t.x === l })); r[c].y.push(h), a.push(h.y1), s.push(h.y2) }, l = 0; l < e[i].data.length; l++)n(l); return { start: a, end: s, rangeUniques: r } } }, { key: "handleCandleStickBoxDataFormat", value: function (t, e, i) { var a = this.w, s = "boxPlot" === a.config.chart.type || "boxPlot" === a.config.series[i].type, r = [], o = [], n = [], l = [], h = []; if ("array" === t) if (s && 6 === e[i].data[0].length || !s && 5 === e[i].data[0].length) for (var c = 0; c < e[i].data.length; c++)r.push(e[i].data[c][1]), o.push(e[i].data[c][2]), s ? (n.push(e[i].data[c][3]), l.push(e[i].data[c][4]), h.push(e[i].data[c][5])) : (l.push(e[i].data[c][3]), h.push(e[i].data[c][4])); else for (var d = 0; d < e[i].data.length; d++)Array.isArray(e[i].data[d][1]) && (r.push(e[i].data[d][1][0]), o.push(e[i].data[d][1][1]), s ? (n.push(e[i].data[d][1][2]), l.push(e[i].data[d][1][3]), h.push(e[i].data[d][1][4])) : (l.push(e[i].data[d][1][2]), h.push(e[i].data[d][1][3]))); else if ("xy" === t) for (var g = 0; g < e[i].data.length; g++)Array.isArray(e[i].data[g].y) && (r.push(e[i].data[g].y[0]), o.push(e[i].data[g].y[1]), s ? (n.push(e[i].data[g].y[2]), l.push(e[i].data[g].y[3]), h.push(e[i].data[g].y[4])) : (l.push(e[i].data[g].y[2]), h.push(e[i].data[g].y[3]))); return { o: r, h: o, m: n, l: l, c: h } } }, { key: "parseDataAxisCharts", value: function (t) { var e = this, i = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : this.ctx, a = this.w.config, s = this.w.globals, r = new A(i), o = a.labels.length > 0 ? a.labels.slice() : a.xaxis.categories.slice(); s.isRangeBar = "rangeBar" === a.chart.type && s.isBarHorizontal, s.hasXaxisGroups = "category" === a.xaxis.type && a.xaxis.group.groups.length > 0, s.hasXaxisGroups && (s.groups = a.xaxis.group.groups), t.forEach((function (t, e) { void 0 !== t.name ? s.seriesNames.push(t.name) : s.seriesNames.push("series-" + parseInt(e + 1, 10)) })), this.coreUtils.setSeriesYAxisMappings(); var n = [], l = u(new Set(a.series.map((function (t) { return t.group })))); a.series.forEach((function (t, e) { var i = l.indexOf(t.group); n[i] || (n[i] = []), n[i].push(s.seriesNames[e]) })), s.seriesGroups = n; for (var h = function () { for (var t = 0; t < o.length; t++)if ("string" == typeof o[t]) { if (!r.isValidDate(o[t])) throw new Error("You have provided invalid Date format. Please provide a valid JavaScript Date"); e.twoDSeriesX.push(r.parseDate(o[t])) } else e.twoDSeriesX.push(o[t]) }, c = 0; c < t.length; c++) { if (this.twoDSeries = [], this.twoDSeriesX = [], this.threeDSeries = [], void 0 === t[c].data) return void console.error("It is a possibility that you may have not included 'data' property in series."); if ("rangeBar" !== a.chart.type && "rangeArea" !== a.chart.type && "rangeBar" !== t[c].type && "rangeArea" !== t[c].type || (s.isRangeData = !0, "rangeBar" !== a.chart.type && "rangeArea" !== a.chart.type || this.handleRangeData(t, c)), this.isMultiFormat()) this.isFormat2DArray() ? this.handleFormat2DArray(t, c) : this.isFormatXY() && this.handleFormatXY(t, c), "candlestick" !== a.chart.type && "candlestick" !== t[c].type && "boxPlot" !== a.chart.type && "boxPlot" !== t[c].type || this.handleCandleStickBoxData(t, c), s.series.push(this.twoDSeries), s.labels.push(this.twoDSeriesX), s.seriesX.push(this.twoDSeriesX), s.seriesGoals = this.seriesGoals, c !== this.activeSeriesIndex || this.fallbackToCategory || (s.isXNumeric = !0); else { "datetime" === a.xaxis.type ? (s.isXNumeric = !0, h(), s.seriesX.push(this.twoDSeriesX)) : "numeric" === a.xaxis.type && (s.isXNumeric = !0, o.length > 0 && (this.twoDSeriesX = o, s.seriesX.push(this.twoDSeriesX))), s.labels.push(this.twoDSeriesX); var d = t[c].data.map((function (t) { return x.parseNumber(t) })); s.series.push(d) } s.seriesZ.push(this.threeDSeries), void 0 !== t[c].color ? s.seriesColors.push(t[c].color) : s.seriesColors.push(void 0) } return this.w } }, { key: "parseDataNonAxisCharts", value: function (t) { var e = this.w.globals, i = this.w.config; e.series = t.slice(), e.seriesNames = i.labels.slice(); for (var a = 0; a < e.series.length; a++)void 0 === e.seriesNames[a] && e.seriesNames.push("series-" + (a + 1)); return this.w } }, { key: "handleExternalLabelsData", value: function (t) { var e = this.w.config, i = this.w.globals; if (e.xaxis.categories.length > 0) i.labels = e.xaxis.categories; else if (e.labels.length > 0) i.labels = e.labels.slice(); else if (this.fallbackToCategory) { if (i.labels = i.labels[0], i.seriesRange.length && (i.seriesRange.map((function (t) { t.forEach((function (t) { i.labels.indexOf(t.x) < 0 && t.x && i.labels.push(t.x) })) })), i.labels = Array.from(new Set(i.labels.map(JSON.stringify)), JSON.parse)), e.xaxis.convertedCatToNumeric) new E(e).convertCatToNumericXaxis(e, this.ctx, i.seriesX[0]), this._generateExternalLabels(t) } else this._generateExternalLabels(t) } }, { key: "_generateExternalLabels", value: function (t) { var e = this.w.globals, i = this.w.config, a = []; if (e.axisCharts) { if (e.series.length > 0) if (this.isFormatXY()) for (var s = i.series.map((function (t, e) { return t.data.filter((function (t, e, i) { return i.findIndex((function (e) { return e.x === t.x })) === e })) })), r = s.reduce((function (t, e, i, a) { return a[t].length > e.length ? t : i }), 0), o = 0; o < s[r].length; o++)a.push(o + 1); else for (var n = 0; n < e.series[e.maxValsInArrayIndex].length; n++)a.push(n + 1); e.seriesX = []; for (var l = 0; l < t.length; l++)e.seriesX.push(a); this.w.globals.isBarHorizontal || (e.isXNumeric = !0) } if (0 === a.length) { a = e.axisCharts ? [] : e.series.map((function (t, e) { return e + 1 })); for (var h = 0; h < t.length; h++)e.seriesX.push(a) } e.labels = a, i.xaxis.convertedCatToNumeric && (e.categoryLabels = a.map((function (t) { return i.xaxis.labels.formatter(t) }))), e.noLabelsProvided = !0 } }, { key: "parseData", value: function (t) { var e = this.w, i = e.config, a = e.globals; if (this.excludeCollapsedSeriesInYAxis(), this.fallbackToCategory = !1, this.ctx.core.resetGlobals(), this.ctx.core.isMultipleY(), a.axisCharts ? (this.parseDataAxisCharts(t), this.coreUtils.getLargestSeries()) : this.parseDataNonAxisCharts(t), i.chart.stacked) { var s = new W(this.ctx); a.series = s.setNullSeriesToZeroValues(a.series) } this.coreUtils.getSeriesTotals(), a.axisCharts && (a.stackedSeriesTotals = this.coreUtils.getStackedSeriesTotals(), a.stackedSeriesTotalsByGroups = this.coreUtils.getStackedSeriesTotalsByGroups()), this.coreUtils.getPercentSeries(), a.dataFormatXNumeric || a.isXNumeric && ("numeric" !== i.xaxis.type || 0 !== i.labels.length || 0 !== i.xaxis.categories.length) || this.handleExternalLabelsData(t); for (var r = this.coreUtils.getCategoryLabels(a.labels), o = 0; o < r.length; o++)if (Array.isArray(r[o])) { a.isMultiLineX = !0; break } } }, { key: "excludeCollapsedSeriesInYAxis", value: function () { var t = this.w, e = []; t.globals.seriesYAxisMap.forEach((function (i, a) { var s = 0; i.forEach((function (e) { -1 !== t.globals.collapsedSeriesIndices.indexOf(e) && s++ })), s > 0 && s == i.length && e.push(a) })), t.globals.ignoreYAxisIndexes = e.map((function (t) { return t })) } }]), t }(), G = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w } return r(t, [{ key: "scaleSvgNode", value: function (t, e) { var i = parseFloat(t.getAttributeNS(null, "width")), a = parseFloat(t.getAttributeNS(null, "height")); t.setAttributeNS(null, "width", i * e), t.setAttributeNS(null, "height", a * e), t.setAttributeNS(null, "viewBox", "0 0 " + i + " " + a) } }, { key: "fixSvgStringForIe11", value: function (t) { if (!x.isIE11()) return t.replace(/ /g, " "); var e = 0, i = t.replace(/xmlns="http:\/\/www.w3.org\/2000\/svg"/g, (function (t) { return 2 === ++e ? 'xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev"' : t })); return i = (i = i.replace(/xmlns:NS\d+=""/g, "")).replace(/NS\d+:(\w+:\w+=")/g, "$1") } }, { key: "getSvgString", value: function (t) { null == t && (t = 1); var e = this.w.globals.dom.Paper.svg(); if (1 !== t) { var i = this.w.globals.dom.Paper.node.cloneNode(!0); this.scaleSvgNode(i, t), e = (new XMLSerializer).serializeToString(i) } return this.fixSvgStringForIe11(e) } }, { key: "cleanup", value: function () { var t = this.w, e = t.globals.dom.baseEl.getElementsByClassName("apexcharts-xcrosshairs"), i = t.globals.dom.baseEl.getElementsByClassName("apexcharts-ycrosshairs"), a = t.globals.dom.baseEl.querySelectorAll(".apexcharts-zoom-rect, .apexcharts-selection-rect"); Array.prototype.forEach.call(a, (function (t) { t.setAttribute("width", 0) })), e && e[0] && (e[0].setAttribute("x", -500), e[0].setAttribute("x1", -500), e[0].setAttribute("x2", -500)), i && i[0] && (i[0].setAttribute("y", -100), i[0].setAttribute("y1", -100), i[0].setAttribute("y2", -100)) } }, { key: "svgUrl", value: function () { this.cleanup(); var t = this.getSvgString(), e = new Blob([t], { type: "image/svg+xml;charset=utf-8" }); return URL.createObjectURL(e) } }, { key: "dataURI", value: function (t) { var e = this; return new Promise((function (i) { var a = e.w, s = t ? t.scale || t.width / a.globals.svgWidth : 1; e.cleanup(); var r = document.createElement("canvas"); r.width = a.globals.svgWidth * s, r.height = parseInt(a.globals.dom.elWrap.style.height, 10) * s; var o = "transparent" === a.config.chart.background ? "#fff" : a.config.chart.background, n = r.getContext("2d"); n.fillStyle = o, n.fillRect(0, 0, r.width * s, r.height * s); var l = e.getSvgString(s); if (window.canvg && x.isIE11()) { var h = window.canvg.Canvg.fromString(n, l, { ignoreClear: !0, ignoreDimensions: !0 }); h.start(); var c = r.msToBlob(); h.stop(), i({ blob: c }) } else { var d = "data:image/svg+xml," + encodeURIComponent(l), g = new Image; g.crossOrigin = "anonymous", g.onload = function () { if (n.drawImage(g, 0, 0), r.msToBlob) { var t = r.msToBlob(); i({ blob: t }) } else { var e = r.toDataURL("image/png"); i({ imgURI: e }) } }, g.src = d } })) } }, { key: "exportToSVG", value: function () { this.triggerDownload(this.svgUrl(), this.w.config.chart.toolbar.export.svg.filename, ".svg") } }, { key: "exportToPng", value: function () { var t = this; this.dataURI().then((function (e) { var i = e.imgURI, a = e.blob; a ? navigator.msSaveOrOpenBlob(a, t.w.globals.chartID + ".png") : t.triggerDownload(i, t.w.config.chart.toolbar.export.png.filename, ".png") })) } }, { key: "exportToCSV", value: function (t) { var e = this, i = t.series, a = t.fileName, s = t.columnDelimiter, r = void 0 === s ? "," : s, o = t.lineDelimiter, n = void 0 === o ? "\n" : o, l = this.w; i || (i = l.config.series); var h, c, d = [], g = [], p = "", f = l.globals.series.map((function (t, e) { return -1 === l.globals.collapsedSeriesIndices.indexOf(e) ? t : [] })), b = function (t) { return "datetime" === l.config.xaxis.type && String(t).length >= 10 }, v = Math.max.apply(Math, u(i.map((function (t) { return t.data ? t.data.length : 0 })))), m = new B(this.ctx), y = new C(this.ctx), w = function (t) { var i = ""; if (l.globals.axisCharts) { if ("category" === l.config.xaxis.type || l.config.xaxis.convertedCatToNumeric) if (l.globals.isBarHorizontal) { var a = l.globals.yLabelFormatters[0], s = new W(e.ctx).getActiveConfigSeriesIndex(); i = a(l.globals.labels[t], { seriesIndex: s, dataPointIndex: t, w: l }) } else i = y.getLabel(l.globals.labels, l.globals.timescaleLabels, 0, t).text; "datetime" === l.config.xaxis.type && (l.config.xaxis.categories.length ? i = l.config.xaxis.categories[t] : l.config.labels.length && (i = l.config.labels[t])) } else i = l.config.labels[t]; return null === i ? "nullvalue" : (Array.isArray(i) && (i = i.join(" ")), x.isNumber(i) ? i : i.split(r).join("")) }, k = function (t, e) { if (d.length && 0 === e && g.push(d.join(r)), t.data) { t.data = t.data.length && t.data || u(Array(v)).map((function () { return "" })); for (var a = 0; a < t.data.length; a++) { d = []; var s = w(a); if ("nullvalue" !== s) { if (s || (m.isFormatXY() ? s = i[e].data[a].x : m.isFormat2DArray() && (s = i[e].data[a] ? i[e].data[a][0] : "")), 0 === e) { d.push(b(s) ? l.config.chart.toolbar.export.csv.dateFormatter(s) : x.isNumber(s) ? s : s.split(r).join("")); for (var o = 0; o < l.globals.series.length; o++) { var n; if (m.isFormatXY()) d.push(null === (n = i[o].data[a]) || void 0 === n ? void 0 : n.y); else d.push(f[o][a]) } } ("candlestick" === l.config.chart.type || t.type && "candlestick" === t.type) && (d.pop(), d.push(l.globals.seriesCandleO[e][a]), d.push(l.globals.seriesCandleH[e][a]), d.push(l.globals.seriesCandleL[e][a]), d.push(l.globals.seriesCandleC[e][a])), ("boxPlot" === l.config.chart.type || t.type && "boxPlot" === t.type) && (d.pop(), d.push(l.globals.seriesCandleO[e][a]), d.push(l.globals.seriesCandleH[e][a]), d.push(l.globals.seriesCandleM[e][a]), d.push(l.globals.seriesCandleL[e][a]), d.push(l.globals.seriesCandleC[e][a])), "rangeBar" === l.config.chart.type && (d.pop(), d.push(l.globals.seriesRangeStart[e][a]), d.push(l.globals.seriesRangeEnd[e][a])), d.length && g.push(d.join(r)) } } } }; d.push(l.config.chart.toolbar.export.csv.headerCategory), "boxPlot" === l.config.chart.type ? (d.push("minimum"), d.push("q1"), d.push("median"), d.push("q3"), d.push("maximum")) : "candlestick" === l.config.chart.type ? (d.push("open"), d.push("high"), d.push("low"), d.push("close")) : "rangeBar" === l.config.chart.type ? (d.push("minimum"), d.push("maximum")) : i.map((function (t, e) { var i = (t.name ? t.name : "series-".concat(e)) + ""; l.globals.axisCharts && d.push(i.split(r).join("") ? i.split(r).join("") : "series-".concat(e)) })), l.globals.axisCharts || (d.push(l.config.chart.toolbar.export.csv.headerValue), g.push(d.join(r))), l.globals.allSeriesHasEqualX || !l.globals.axisCharts || l.config.xaxis.categories.length || l.config.labels.length ? i.map((function (t, e) { l.globals.axisCharts ? k(t, e) : ((d = []).push(l.globals.labels[e].split(r).join("")), d.push(f[e]), g.push(d.join(r))) })) : (h = new Set, c = {}, i.forEach((function (t, e) { null == t || t.data.forEach((function (t) { var a, s; if (m.isFormatXY()) a = t.x, s = t.y; else { if (!m.isFormat2DArray()) return; a = t[0], s = t[1] } c[a] || (c[a] = Array(i.length).fill("")), c[a][e] = s, h.add(a) })) })), d.length && g.push(d.join(r)), Array.from(h).sort().forEach((function (t) { g.push([b(t) && "datetime" === l.config.xaxis.type ? l.config.chart.toolbar.export.csv.dateFormatter(t) : x.isNumber(t) ? t : t.split(r).join(""), c[t].join(r)]) }))), p += g.join(n), this.triggerDownload("data:text/csv; charset=utf-8," + encodeURIComponent("\ufeff" + p), a || l.config.chart.toolbar.export.csv.filename, ".csv") } }, { key: "triggerDownload", value: function (t, e, i) { var a = document.createElement("a"); a.href = t, a.download = (e || this.w.globals.chartID) + i, document.body.appendChild(a), a.click(), document.body.removeChild(a) } }]), t }(), V = function () { function t(e, i) { a(this, t), this.ctx = e, this.elgrid = i, this.w = e.w; var s = this.w; this.axesUtils = new C(e), this.xaxisLabels = s.globals.labels.slice(), s.globals.timescaleLabels.length > 0 && !s.globals.isBarHorizontal && (this.xaxisLabels = s.globals.timescaleLabels.slice()), s.config.xaxis.overwriteCategories && (this.xaxisLabels = s.config.xaxis.overwriteCategories), this.drawnLabels = [], this.drawnLabelsRects = [], "top" === s.config.xaxis.position ? this.offY = 0 : this.offY = s.globals.gridHeight, this.offY = this.offY + s.config.xaxis.axisBorder.offsetY, this.isCategoryBarHorizontal = "bar" === s.config.chart.type && s.config.plotOptions.bar.horizontal, this.xaxisFontSize = s.config.xaxis.labels.style.fontSize, this.xaxisFontFamily = s.config.xaxis.labels.style.fontFamily, this.xaxisForeColors = s.config.xaxis.labels.style.colors, this.xaxisBorderWidth = s.config.xaxis.axisBorder.width, this.isCategoryBarHorizontal && (this.xaxisBorderWidth = s.config.yaxis[0].axisBorder.width.toString()), this.xaxisBorderWidth.indexOf("%") > -1 ? this.xaxisBorderWidth = s.globals.gridWidth * parseInt(this.xaxisBorderWidth, 10) / 100 : this.xaxisBorderWidth = parseInt(this.xaxisBorderWidth, 10), this.xaxisBorderHeight = s.config.xaxis.axisBorder.height, this.yaxis = s.config.yaxis[0] } return r(t, [{ key: "drawXaxis", value: function () { var t = this.w, e = new m(this.ctx), i = e.group({ class: "apexcharts-xaxis", transform: "translate(".concat(t.config.xaxis.offsetX, ", ").concat(t.config.xaxis.offsetY, ")") }), a = e.group({ class: "apexcharts-xaxis-texts-g", transform: "translate(".concat(t.globals.translateXAxisX, ", ").concat(t.globals.translateXAxisY, ")") }); i.add(a); for (var s = [], r = 0; r < this.xaxisLabels.length; r++)s.push(this.xaxisLabels[r]); if (this.drawXAxisLabelAndGroup(!0, e, a, s, t.globals.isXNumeric, (function (t, e) { return e })), t.globals.hasXaxisGroups) { var o = t.globals.groups; s = []; for (var n = 0; n < o.length; n++)s.push(o[n].title); var l = {}; t.config.xaxis.group.style && (l.xaxisFontSize = t.config.xaxis.group.style.fontSize, l.xaxisFontFamily = t.config.xaxis.group.style.fontFamily, l.xaxisForeColors = t.config.xaxis.group.style.colors, l.fontWeight = t.config.xaxis.group.style.fontWeight, l.cssClass = t.config.xaxis.group.style.cssClass), this.drawXAxisLabelAndGroup(!1, e, a, s, !1, (function (t, e) { return o[t].cols * e }), l) } if (void 0 !== t.config.xaxis.title.text) { var h = e.group({ class: "apexcharts-xaxis-title" }), c = e.drawText({ x: t.globals.gridWidth / 2 + t.config.xaxis.title.offsetX, y: this.offY + parseFloat(this.xaxisFontSize) + ("bottom" === t.config.xaxis.position ? t.globals.xAxisLabelsHeight : -t.globals.xAxisLabelsHeight - 10) + t.config.xaxis.title.offsetY, text: t.config.xaxis.title.text, textAnchor: "middle", fontSize: t.config.xaxis.title.style.fontSize, fontFamily: t.config.xaxis.title.style.fontFamily, fontWeight: t.config.xaxis.title.style.fontWeight, foreColor: t.config.xaxis.title.style.color, cssClass: "apexcharts-xaxis-title-text " + t.config.xaxis.title.style.cssClass }); h.add(c), i.add(h) } if (t.config.xaxis.axisBorder.show) { var d = t.globals.barPadForNumericAxis, g = e.drawLine(t.globals.padHorizontal + t.config.xaxis.axisBorder.offsetX - d, this.offY, this.xaxisBorderWidth + d, this.offY, t.config.xaxis.axisBorder.color, 0, this.xaxisBorderHeight); this.elgrid && this.elgrid.elGridBorders && t.config.grid.show ? this.elgrid.elGridBorders.add(g) : i.add(g) } return i } }, { key: "drawXAxisLabelAndGroup", value: function (t, e, i, a, s, r) { var o, n = this, l = arguments.length > 6 && void 0 !== arguments[6] ? arguments[6] : {}, h = [], c = [], d = this.w, g = l.xaxisFontSize || this.xaxisFontSize, u = l.xaxisFontFamily || this.xaxisFontFamily, p = l.xaxisForeColors || this.xaxisForeColors, f = l.fontWeight || d.config.xaxis.labels.style.fontWeight, x = l.cssClass || d.config.xaxis.labels.style.cssClass, b = d.globals.padHorizontal, v = a.length, m = "category" === d.config.xaxis.type ? d.globals.dataPoints : v; if (0 === m && v > m && (m = v), s) { var y = m > 1 ? m - 1 : m; o = d.globals.gridWidth / Math.min(y, v - 1), b = b + r(0, o) / 2 + d.config.xaxis.labels.offsetX } else o = d.globals.gridWidth / m, b = b + r(0, o) + d.config.xaxis.labels.offsetX; for (var w = function (s) { var l = b - r(s, o) / 2 + d.config.xaxis.labels.offsetX; 0 === s && 1 === v && o / 2 === b && 1 === m && (l = d.globals.gridWidth / 2); var y = n.axesUtils.getLabel(a, d.globals.timescaleLabels, l, s, h, g, t), w = 28; d.globals.rotateXLabels && t && (w = 22), d.config.xaxis.title.text && "top" === d.config.xaxis.position && (w += parseFloat(d.config.xaxis.title.style.fontSize) + 2), t || (w = w + parseFloat(g) + (d.globals.xAxisLabelsHeight - d.globals.xAxisGroupLabelsHeight) + (d.globals.rotateXLabels ? 10 : 0)), y = void 0 !== d.config.xaxis.tickAmount && "dataPoints" !== d.config.xaxis.tickAmount && "datetime" !== d.config.xaxis.type ? n.axesUtils.checkLabelBasedOnTickamount(s, y, v) : n.axesUtils.checkForOverflowingLabels(s, y, v, h, c); if (d.config.xaxis.labels.show) { var k = e.drawText({ x: y.x, y: n.offY + d.config.xaxis.labels.offsetY + w - ("top" === d.config.xaxis.position ? d.globals.xAxisHeight + d.config.xaxis.axisTicks.height - 2 : 0), text: y.text, textAnchor: "middle", fontWeight: y.isBold ? 600 : f, fontSize: g, fontFamily: u, foreColor: Array.isArray(p) ? t && d.config.xaxis.convertedCatToNumeric ? p[d.globals.minX + s - 1] : p[s] : p, isPlainText: !1, cssClass: (t ? "apexcharts-xaxis-label " : "apexcharts-xaxis-group-label ") + x }); if (i.add(k), k.on("click", (function (t) { if ("function" == typeof d.config.chart.events.xAxisLabelClick) { var e = Object.assign({}, d, { labelIndex: s }); d.config.chart.events.xAxisLabelClick(t, n.ctx, e) } })), t) { var A = document.createElementNS(d.globals.SVGNS, "title"); A.textContent = Array.isArray(y.text) ? y.text.join(" ") : y.text, k.node.appendChild(A), "" !== y.text && (h.push(y.text), c.push(y)) } } s < v - 1 && (b += r(s + 1, o)) }, k = 0; k <= v - 1; k++)w(k) } }, { key: "drawXaxisInversed", value: function (t) { var e, i, a = this, s = this.w, r = new m(this.ctx), o = s.config.yaxis[0].opposite ? s.globals.translateYAxisX[t] : 0, n = r.group({ class: "apexcharts-yaxis apexcharts-xaxis-inversed", rel: t }), l = r.group({ class: "apexcharts-yaxis-texts-g apexcharts-xaxis-inversed-texts-g", transform: "translate(" + o + ", 0)" }); n.add(l); var h = []; if (s.config.yaxis[t].show) for (var c = 0; c < this.xaxisLabels.length; c++)h.push(this.xaxisLabels[c]); e = s.globals.gridHeight / h.length, i = -e / 2.2; var d = s.globals.yLabelFormatters[0], g = s.config.yaxis[0].labels; if (g.show) for (var u = function (o) { var n = void 0 === h[o] ? "" : h[o]; n = d(n, { seriesIndex: t, dataPointIndex: o, w: s }); var c = a.axesUtils.getYAxisForeColor(g.style.colors, t), u = 0; Array.isArray(n) && (u = n.length / 2 * parseInt(g.style.fontSize, 10)); var p = g.offsetX - 15, f = "end"; a.yaxis.opposite && (f = "start"), "left" === s.config.yaxis[0].labels.align ? (p = g.offsetX, f = "start") : "center" === s.config.yaxis[0].labels.align ? (p = g.offsetX, f = "middle") : "right" === s.config.yaxis[0].labels.align && (f = "end"); var x = r.drawText({ x: p, y: i + e + g.offsetY - u, text: n, textAnchor: f, foreColor: Array.isArray(c) ? c[o] : c, fontSize: g.style.fontSize, fontFamily: g.style.fontFamily, fontWeight: g.style.fontWeight, isPlainText: !1, cssClass: "apexcharts-yaxis-label " + g.style.cssClass, maxWidth: g.maxWidth }); l.add(x), x.on("click", (function (t) { if ("function" == typeof s.config.chart.events.xAxisLabelClick) { var e = Object.assign({}, s, { labelIndex: o }); s.config.chart.events.xAxisLabelClick(t, a.ctx, e) } })); var b = document.createElementNS(s.globals.SVGNS, "title"); if (b.textContent = Array.isArray(n) ? n.join(" ") : n, x.node.appendChild(b), 0 !== s.config.yaxis[t].labels.rotate) { var v = r.rotateAroundCenter(x.node); x.node.setAttribute("transform", "rotate(".concat(s.config.yaxis[t].labels.rotate, " 0 ").concat(v.y, ")")) } i += e }, p = 0; p <= h.length - 1; p++)u(p); if (void 0 !== s.config.yaxis[0].title.text) { var f = r.group({ class: "apexcharts-yaxis-title apexcharts-xaxis-title-inversed", transform: "translate(" + o + ", 0)" }), x = r.drawText({ x: s.config.yaxis[0].title.offsetX, y: s.globals.gridHeight / 2 + s.config.yaxis[0].title.offsetY, text: s.config.yaxis[0].title.text, textAnchor: "middle", foreColor: s.config.yaxis[0].title.style.color, fontSize: s.config.yaxis[0].title.style.fontSize, fontWeight: s.config.yaxis[0].title.style.fontWeight, fontFamily: s.config.yaxis[0].title.style.fontFamily, cssClass: "apexcharts-yaxis-title-text " + s.config.yaxis[0].title.style.cssClass }); f.add(x), n.add(f) } var b = 0; this.isCategoryBarHorizontal && s.config.yaxis[0].opposite && (b = s.globals.gridWidth); var v = s.config.xaxis.axisBorder; if (v.show) { var y = r.drawLine(s.globals.padHorizontal + v.offsetX + b, 1 + v.offsetY, s.globals.padHorizontal + v.offsetX + b, s.globals.gridHeight + v.offsetY, v.color, 0); this.elgrid && this.elgrid.elGridBorders && s.config.grid.show ? this.elgrid.elGridBorders.add(y) : n.add(y) } return s.config.yaxis[0].axisTicks.show && this.axesUtils.drawYAxisTicks(b, h.length, s.config.yaxis[0].axisBorder, s.config.yaxis[0].axisTicks, 0, e, n), n } }, { key: "drawXaxisTicks", value: function (t, e, i) { var a = this.w, s = t; if (!(t < 0 || t - 2 > a.globals.gridWidth)) { var r = this.offY + a.config.xaxis.axisTicks.offsetY; if (e = e + r + a.config.xaxis.axisTicks.height, "top" === a.config.xaxis.position && (e = r - a.config.xaxis.axisTicks.height), a.config.xaxis.axisTicks.show) { var o = new m(this.ctx).drawLine(t + a.config.xaxis.axisTicks.offsetX, r + a.config.xaxis.offsetY, s + a.config.xaxis.axisTicks.offsetX, e + a.config.xaxis.offsetY, a.config.xaxis.axisTicks.color); i.add(o), o.node.classList.add("apexcharts-xaxis-tick") } } } }, { key: "getXAxisTicksPositions", value: function () { var t = this.w, e = [], i = this.xaxisLabels.length, a = t.globals.padHorizontal; if (t.globals.timescaleLabels.length > 0) for (var s = 0; s < i; s++)a = this.xaxisLabels[s].position, e.push(a); else for (var r = i, o = 0; o < r; o++) { var n = r; t.globals.isXNumeric && "bar" !== t.config.chart.type && (n -= 1), a += t.globals.gridWidth / n, e.push(a) } return e } }, { key: "xAxisLabelCorrections", value: function () { var t = this.w, e = new m(this.ctx), i = t.globals.dom.baseEl.querySelector(".apexcharts-xaxis-texts-g"), a = t.globals.dom.baseEl.querySelectorAll(".apexcharts-xaxis-texts-g text:not(.apexcharts-xaxis-group-label)"), s = t.globals.dom.baseEl.querySelectorAll(".apexcharts-yaxis-inversed text"), r = t.globals.dom.baseEl.querySelectorAll(".apexcharts-xaxis-inversed-texts-g text tspan"); if (t.globals.rotateXLabels || t.config.xaxis.labels.rotateAlways) for (var o = 0; o < a.length; o++) { var n = e.rotateAroundCenter(a[o]); n.y = n.y - 1, n.x = n.x + 1, a[o].setAttribute("transform", "rotate(".concat(t.config.xaxis.labels.rotate, " ").concat(n.x, " ").concat(n.y, ")")), a[o].setAttribute("text-anchor", "end"); i.setAttribute("transform", "translate(0, ".concat(-10, ")")); var l = a[o].childNodes; t.config.xaxis.labels.trim && Array.prototype.forEach.call(l, (function (i) { e.placeTextWithEllipsis(i, i.textContent, t.globals.xAxisLabelsHeight - ("bottom" === t.config.legend.position ? 20 : 10)) })) } else !function () { for (var i = t.globals.gridWidth / (t.globals.labels.length + 1), s = 0; s < a.length; s++) { var r = a[s].childNodes; t.config.xaxis.labels.trim && "datetime" !== t.config.xaxis.type && Array.prototype.forEach.call(r, (function (t) { e.placeTextWithEllipsis(t, t.textContent, i) })) } }(); if (s.length > 0) { var h = s[s.length - 1].getBBox(), c = s[0].getBBox(); h.x < -20 && s[s.length - 1].parentNode.removeChild(s[s.length - 1]), c.x + c.width > t.globals.gridWidth && !t.globals.isBarHorizontal && s[0].parentNode.removeChild(s[0]); for (var d = 0; d < r.length; d++)e.placeTextWithEllipsis(r[d], r[d].textContent, t.config.yaxis[0].labels.maxWidth - (t.config.yaxis[0].title.text ? 2 * parseFloat(t.config.yaxis[0].title.style.fontSize) : 0) - 15) } } }]), t }(), j = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w; var i = this.w; this.xaxisLabels = i.globals.labels.slice(), this.axesUtils = new C(e), this.isRangeBar = i.globals.seriesRange.length && i.globals.isBarHorizontal, i.globals.timescaleLabels.length > 0 && (this.xaxisLabels = i.globals.timescaleLabels.slice()) } return r(t, [{ key: "drawGridArea", value: function () { var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : null, e = this.w, i = new m(this.ctx); null === t && (t = i.group({ class: "apexcharts-grid" })); var a = i.drawLine(e.globals.padHorizontal, 1, e.globals.padHorizontal, e.globals.gridHeight, "transparent"), s = i.drawLine(e.globals.padHorizontal, e.globals.gridHeight, e.globals.gridWidth, e.globals.gridHeight, "transparent"); return t.add(s), t.add(a), t } }, { key: "drawGrid", value: function () { var t = null; return this.w.globals.axisCharts && (t = this.renderGrid(), this.drawGridArea(t.el)), t } }, { key: "createGridMask", value: function () { var t = this.w, e = t.globals, i = new m(this.ctx), a = Array.isArray(t.config.stroke.width) ? 0 : t.config.stroke.width; if (Array.isArray(t.config.stroke.width)) { var s = 0; t.config.stroke.width.forEach((function (t) { s = Math.max(s, t) })), a = s } e.dom.elGridRectMask = document.createElementNS(e.SVGNS, "clipPath"), e.dom.elGridRectMask.setAttribute("id", "gridRectMask".concat(e.cuid)), e.dom.elGridRectMarkerMask = document.createElementNS(e.SVGNS, "clipPath"), e.dom.elGridRectMarkerMask.setAttribute("id", "gridRectMarkerMask".concat(e.cuid)), e.dom.elForecastMask = document.createElementNS(e.SVGNS, "clipPath"), e.dom.elForecastMask.setAttribute("id", "forecastMask".concat(e.cuid)), e.dom.elNonForecastMask = document.createElementNS(e.SVGNS, "clipPath"), e.dom.elNonForecastMask.setAttribute("id", "nonForecastMask".concat(e.cuid)); var r = t.config.chart.type, o = 0, n = 0; ("bar" === r || "rangeBar" === r || "candlestick" === r || "boxPlot" === r || t.globals.comboBarCount > 0) && t.globals.isXNumeric && !t.globals.isBarHorizontal && (o = t.config.grid.padding.left, n = t.config.grid.padding.right, e.barPadForNumericAxis > o && (o = e.barPadForNumericAxis, n = e.barPadForNumericAxis)), e.dom.elGridRect = i.drawRect(-a / 2 - o - 2, -a / 2 - 2, e.gridWidth + a + n + o + 4, e.gridHeight + a + 4, 0, "#fff"); var l = t.globals.markers.largestSize + 1; e.dom.elGridRectMarker = i.drawRect(2 * -l, 2 * -l, e.gridWidth + 4 * l, e.gridHeight + 4 * l, 0, "#fff"), e.dom.elGridRectMask.appendChild(e.dom.elGridRect.node), e.dom.elGridRectMarkerMask.appendChild(e.dom.elGridRectMarker.node); var h = e.dom.baseEl.querySelector("defs"); h.appendChild(e.dom.elGridRectMask), h.appendChild(e.dom.elForecastMask), h.appendChild(e.dom.elNonForecastMask), h.appendChild(e.dom.elGridRectMarkerMask) } }, { key: "_drawGridLines", value: function (t) { var e = t.i, i = t.x1, a = t.y1, s = t.x2, r = t.y2, o = t.xCount, n = t.parent, l = this.w; if (!(0 === e && l.globals.skipFirstTimelinelabel || e === o - 1 && l.globals.skipLastTimelinelabel && !l.config.xaxis.labels.formatter || "radar" === l.config.chart.type)) { l.config.grid.xaxis.lines.show && this._drawGridLine({ i: e, x1: i, y1: a, x2: s, y2: r, xCount: o, parent: n }); var h = 0; if (l.globals.hasXaxisGroups && "between" === l.config.xaxis.tickPlacement) { var c = l.globals.groups; if (c) { for (var d = 0, g = 0; d < e && g < c.length; g++)d += c[g].cols; d === e && (h = .6 * l.globals.xAxisLabelsHeight) } } new V(this.ctx).drawXaxisTicks(i, h, l.globals.dom.elGraphical) } } }, { key: "_drawGridLine", value: function (t) { var e = t.i, i = t.x1, a = t.y1, s = t.x2, r = t.y2, o = t.xCount, n = t.parent, l = this.w, h = !1, c = n.node.classList.contains("apexcharts-gridlines-horizontal"), d = l.config.grid.strokeDashArray, g = l.globals.barPadForNumericAxis; (0 === a && 0 === r || 0 === i && 0 === s) && (h = !0), a === l.globals.gridHeight && r === l.globals.gridHeight && (h = !0), !l.globals.isBarHorizontal || 0 !== e && e !== o - 1 || (h = !0); var u = new m(this).drawLine(i - (c ? g : 0), a, s + (c ? g : 0), r, l.config.grid.borderColor, d); u.node.classList.add("apexcharts-gridline"), h && l.config.grid.show ? this.elGridBorders.add(u) : n.add(u) } }, { key: "_drawGridBandRect", value: function (t) { var e = t.c, i = t.x1, a = t.y1, s = t.x2, r = t.y2, o = t.type, n = this.w, l = new m(this.ctx), h = n.globals.barPadForNumericAxis; if ("column" !== o || "datetime" !== n.config.xaxis.type) { var c = n.config.grid[o].colors[e], d = l.drawRect(i - ("row" === o ? h : 0), a, s + ("row" === o ? 2 * h : 0), r, 0, c, n.config.grid[o].opacity); this.elg.add(d), d.attr("clip-path", "url(#gridRectMask".concat(n.globals.cuid, ")")), d.node.classList.add("apexcharts-grid-".concat(o)) } } }, { key: "_drawXYLines", value: function (t) { var e = this, i = t.xCount, a = t.tickAmount, s = this.w; if (s.config.grid.xaxis.lines.show || s.config.xaxis.axisTicks.show) { var r, o = s.globals.padHorizontal, n = s.globals.gridHeight; s.globals.timescaleLabels.length ? function (t) { for (var a = t.xC, s = t.x1, r = t.y1, o = t.x2, n = t.y2, l = 0; l < a; l++)s = e.xaxisLabels[l].position, o = e.xaxisLabels[l].position, e._drawGridLines({ i: l, x1: s, y1: r, x2: o, y2: n, xCount: i, parent: e.elgridLinesV }) }({ xC: i, x1: o, y1: 0, x2: r, y2: n }) : (s.globals.isXNumeric && (i = s.globals.xAxisScale.result.length), function (t) { for (var a = t.xC, r = t.x1, o = t.y1, n = t.x2, l = t.y2, h = 0; h < a + (s.globals.isXNumeric ? 0 : 1); h++)0 === h && 1 === a && 1 === s.globals.dataPoints && (n = r = s.globals.gridWidth / 2), e._drawGridLines({ i: h, x1: r, y1: o, x2: n, y2: l, xCount: i, parent: e.elgridLinesV }), n = r += s.globals.gridWidth / (s.globals.isXNumeric ? a - 1 : a) }({ xC: i, x1: o, y1: 0, x2: r, y2: n })) } if (s.config.grid.yaxis.lines.show) { var l = 0, h = 0, c = s.globals.gridWidth, d = a + 1; this.isRangeBar && (d = s.globals.labels.length); for (var g = 0; g < d + (this.isRangeBar ? 1 : 0); g++)this._drawGridLine({ i: g, xCount: d + (this.isRangeBar ? 1 : 0), x1: 0, y1: l, x2: c, y2: h, parent: this.elgridLinesH }), h = l += s.globals.gridHeight / (this.isRangeBar ? d : a) } } }, { key: "_drawInvertedXYLines", value: function (t) { var e = t.xCount, i = this.w; if (i.config.grid.xaxis.lines.show || i.config.xaxis.axisTicks.show) for (var a, s = i.globals.padHorizontal, r = i.globals.gridHeight, o = 0; o < e + 1; o++) { i.config.grid.xaxis.lines.show && this._drawGridLine({ i: o, xCount: e + 1, x1: s, y1: 0, x2: a, y2: r, parent: this.elgridLinesV }), new V(this.ctx).drawXaxisTicks(s, 0, i.globals.dom.elGraphical), a = s += i.globals.gridWidth / e } if (i.config.grid.yaxis.lines.show) for (var n = 0, l = 0, h = i.globals.gridWidth, c = 0; c < i.globals.dataPoints + 1; c++)this._drawGridLine({ i: c, xCount: i.globals.dataPoints + 1, x1: 0, y1: n, x2: h, y2: l, parent: this.elgridLinesH }), l = n += i.globals.gridHeight / i.globals.dataPoints } }, { key: "renderGrid", value: function () { var t = this.w, e = t.globals, i = new m(this.ctx); this.elg = i.group({ class: "apexcharts-grid" }), this.elgridLinesH = i.group({ class: "apexcharts-gridlines-horizontal" }), this.elgridLinesV = i.group({ class: "apexcharts-gridlines-vertical" }), this.elGridBorders = i.group({ class: "apexcharts-grid-borders" }), this.elg.add(this.elgridLinesH), this.elg.add(this.elgridLinesV), t.config.grid.show || (this.elgridLinesV.hide(), this.elgridLinesH.hide(), this.elGridBorders.hide()); for (var a = 0; a < e.seriesYAxisMap.length && -1 !== e.ignoreYAxisIndexes.indexOf(a);)a++; a === e.seriesYAxisMap.length && (a = 0); var s, r = e.yAxisScale[a].result.length - 1; if (!e.isBarHorizontal || this.isRangeBar) { var o, n, l; if (s = this.xaxisLabels.length, this.isRangeBar) r = e.labels.length, t.config.xaxis.tickAmount && t.config.xaxis.labels.formatter && (s = t.config.xaxis.tickAmount), (null === (o = e.yAxisScale) || void 0 === o || null === (n = o[a]) || void 0 === n || null === (l = n.result) || void 0 === l ? void 0 : l.length) > 0 && "datetime" !== t.config.xaxis.type && (s = e.yAxisScale[a].result.length - 1); this._drawXYLines({ xCount: s, tickAmount: r }) } else s = r, r = e.xTickAmount, this._drawInvertedXYLines({ xCount: s, tickAmount: r }); return this.drawGridBands(s, r), { el: this.elg, elGridBorders: this.elGridBorders, xAxisTickWidth: e.gridWidth / s } } }, { key: "drawGridBands", value: function (t, e) { var i = this.w; if (void 0 !== i.config.grid.row.colors && i.config.grid.row.colors.length > 0) for (var a = 0, s = i.globals.gridHeight / e, r = i.globals.gridWidth, o = 0, n = 0; o < e; o++, n++)n >= i.config.grid.row.colors.length && (n = 0), this._drawGridBandRect({ c: n, x1: 0, y1: a, x2: r, y2: s, type: "row" }), a += i.globals.gridHeight / e; if (void 0 !== i.config.grid.column.colors && i.config.grid.column.colors.length > 0) for (var l = i.globals.isBarHorizontal || "on" !== i.config.xaxis.tickPlacement || "category" !== i.config.xaxis.type && !i.config.xaxis.convertedCatToNumeric ? t : t - 1, h = i.globals.padHorizontal, c = i.globals.padHorizontal + i.globals.gridWidth / l, d = i.globals.gridHeight, g = 0, u = 0; g < t; g++, u++)u >= i.config.grid.column.colors.length && (u = 0), this._drawGridBandRect({ c: u, x1: h, y1: 0, x2: c, y2: d, type: "column" }), h += i.globals.gridWidth / l } }]), t }(), _ = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w } return r(t, [{ key: "niceScale", value: function (t, e) { var i, a, s, r, o = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : 0, n = 1e-11, l = this.w, h = l.globals; h.isBarHorizontal ? (i = l.config.xaxis, a = Math.max((h.svgWidth - 100) / 25, 2)) : (i = l.config.yaxis[o], a = Math.max((h.svgHeight - 100) / 15, 2)), s = void 0 !== i.min && null !== i.min, r = void 0 !== i.max && null !== i.min; var c = void 0 !== i.stepSize && null !== i.stepSize, d = void 0 !== i.tickAmount && null !== i.tickAmount, g = d ? i.tickAmount : i.forceNiceScale ? h.niceScaleDefaultTicks[Math.min(Math.round(a / 2), h.niceScaleDefaultTicks.length - 1)] : 10; if (h.isMultipleYAxis && !d && h.multiAxisTickAmount > 0 && (g = h.multiAxisTickAmount, d = !0), g = "dataPoints" === g ? h.dataPoints - 1 : Math.abs(Math.round(g)), (t === Number.MIN_VALUE && 0 === e || !x.isNumber(t) && !x.isNumber(e) || t === Number.MIN_VALUE && e === -Number.MAX_VALUE) && (t = x.isNumber(i.min) ? i.min : 0, e = x.isNumber(i.max) ? i.max : t + g, h.allSeriesCollapsed = !1), t > e) { console.warn("axis.min cannot be greater than axis.max: swapping min and max"); var u = e; e = t, t = u } else t === e && (t = 0 === t ? 0 : t - 1, e = 0 === e ? 2 : e + 1); var p = []; g < 1 && (g = 1); var f = g, b = Math.abs(e - t); if (i.forceNiceScale) { !s && t > 0 && t / b < .15 && (t = 0, s = !0), !r && e < 0 && -e / b < .15 && (e = 0, r = !0), b = Math.abs(e - t) } var v = b / f, m = v, y = Math.floor(Math.log10(m)), w = Math.pow(10, y), k = Math.ceil(m / w); if (v = m = (k = h.niceScaleAllowedMagMsd[0 === h.yValueDecimal ? 0 : 1][k]) * w, h.isBarHorizontal && i.stepSize && "datetime" !== i.type ? (v = i.stepSize, c = !0) : c && (v = i.stepSize), c && i.forceNiceScale) { var A = Math.floor(Math.log10(v)); v *= Math.pow(10, y - A) } if (s && r) { var S = b / f; if (d) if (c) if (0 != x.mod(b, v)) { var C = x.getGCD(v, S); v = S / C < 10 ? C : S } else 0 == x.mod(v, S) ? v = S : (S = v, d = !1); else v = S; else if (c) 0 == x.mod(b, v) ? S = v : v = S; else if (0 == x.mod(b, v)) S = v; else { S = b / (f = Math.ceil(b / v)); var L = x.getGCD(b, v); b / L < a && (S = L), v = S } f = Math.round(b / v) } else { if (s || r) { if (r) if (d) t = e - v * f; else { var P = t; t = v * Math.floor(t / v), Math.abs(e - t) / x.getGCD(b, v) > a && (t = e - v * g, t += v * Math.floor((P - t) / v)) } else if (s) if (d) e = t + v * f; else { var M = e; e = v * Math.ceil(e / v), Math.abs(e - t) / x.getGCD(b, v) > a && (e = t + v * g, e += v * Math.ceil((M - e) / v)) } } else if (d) { var I = v / (e - t > e ? 1 : 2), T = I * Math.floor(t / I); Math.abs(T - t) <= I / 2 ? e = (t = T) + v * f : t = (e = I * Math.ceil(e / I)) - v * f } else t = v * Math.floor(t / v), e = v * Math.ceil(e / v); b = Math.abs(e - t), v = x.getGCD(b, v), f = Math.round(b / v) } if (d || s || r || (f = Math.ceil((b - n) / (v + n))) > 16 && x.getPrimeFactors(f).length < 2 && f++, !d && i.forceNiceScale && 0 === h.yValueDecimal && f > b && (f = b, v = Math.round(b / f)), f > a && (!d && !c || i.forceNiceScale)) { var z = x.getPrimeFactors(f), X = z.length - 1, E = f; t: for (var Y = 0; Y < X; Y++)for (var F = 0; F <= X - Y; F++) { for (var R = Math.min(F + Y, X), H = E, D = 1, O = F; O <= R; O++)D *= z[O]; if ((H /= D) < a) { E = H; break t } } v = E === f ? b : b / E, f = Math.round(b / v) } h.isMultipleYAxis && 0 == h.multiAxisTickAmount && h.ignoreYAxisIndexes.indexOf(o) < 0 && (h.multiAxisTickAmount = f); var N = t - v, W = v * n; do { N += v, p.push(x.stripNumber(N, 7)) } while (e - N > W); return { result: p, niceMin: p[0], niceMax: p[p.length - 1] } } }, { key: "linearScale", value: function (t, e) { var i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : 10, a = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : 0, s = arguments.length > 4 && void 0 !== arguments[4] ? arguments[4] : void 0, r = Math.abs(e - t); "dataPoints" === (i = this._adjustTicksForSmallRange(i, a, r)) && (i = this.w.globals.dataPoints - 1), s || (s = r / i), i === Number.MAX_VALUE && (i = 5, s = 1); for (var o = [], n = t; i >= 0;)o.push(n), n += s, i -= 1; return { result: o, niceMin: o[0], niceMax: o[o.length - 1] } } }, { key: "logarithmicScaleNice", value: function (t, e, i) { e <= 0 && (e = Math.max(t, i)), t <= 0 && (t = Math.min(e, i)); for (var a = [], s = Math.ceil(Math.log(e) / Math.log(i) + 1), r = Math.floor(Math.log(t) / Math.log(i)); r < s; r++)a.push(Math.pow(i, r)); return { result: a, niceMin: a[0], niceMax: a[a.length - 1] } } }, { key: "logarithmicScale", value: function (t, e, i) { e <= 0 && (e = Math.max(t, i)), t <= 0 && (t = Math.min(e, i)); for (var a = [], s = Math.log(e) / Math.log(i), r = Math.log(t) / Math.log(i), o = s - r, n = Math.round(o), l = o / n, h = 0, c = r; h < n; h++, c += l)a.push(Math.pow(i, c)); return a.push(Math.pow(i, s)), { result: a, niceMin: t, niceMax: e } } }, { key: "_adjustTicksForSmallRange", value: function (t, e, i) { var a = t; if (void 0 !== e && this.w.config.yaxis[e].labels.formatter && void 0 === this.w.config.yaxis[e].tickAmount) { var s = Number(this.w.config.yaxis[e].labels.formatter(1)); x.isNumber(s) && 0 === this.w.globals.yValueDecimal && (a = Math.ceil(i)) } return a < t ? a : t } }, { key: "setYScaleForIndex", value: function (t, e, i) { var a = this.w.globals, s = this.w.config, r = a.isBarHorizontal ? s.xaxis : s.yaxis[t]; void 0 === a.yAxisScale[t] && (a.yAxisScale[t] = []); var o = Math.abs(i - e); r.logarithmic && o <= 5 && (a.invalidLogScale = !0), r.logarithmic && o > 5 ? (a.allSeriesCollapsed = !1, a.yAxisScale[t] = r.forceNiceScale ? this.logarithmicScaleNice(e, i, r.logBase) : this.logarithmicScale(e, i, r.logBase)) : i !== -Number.MAX_VALUE && x.isNumber(i) && e !== Number.MAX_VALUE && x.isNumber(e) ? (a.allSeriesCollapsed = !1, a.yAxisScale[t] = this.niceScale(e, i, t)) : a.yAxisScale[t] = this.niceScale(Number.MIN_VALUE, 0, t) } }, { key: "setXScale", value: function (t, e) { var i = this.w, a = i.globals, s = Math.abs(e - t); return e !== -Number.MAX_VALUE && x.isNumber(e) ? a.xAxisScale = this.linearScale(t, e, i.config.xaxis.tickAmount ? i.config.xaxis.tickAmount : s < 10 && s > 1 ? s + 1 : 10, 0, i.config.xaxis.stepSize) : a.xAxisScale = this.linearScale(0, 10, 10), a.xAxisScale } }, { key: "setSeriesYAxisMappings", value: function () { var t = this.w.globals, e = this.w.config; t.minYArr, t.maxYArr; var i = [], a = [], s = [], r = t.series.length > e.yaxis.length || e.yaxis.some((function (t) { return Array.isArray(t.seriesName) })); e.series.forEach((function (t, e) { s.push(e), a.push(null) })), e.yaxis.forEach((function (t, e) { i[e] = [] })); var o = []; e.yaxis.forEach((function (t, a) { var n = !1; if (t.seriesName) { var l = []; Array.isArray(t.seriesName) ? l = t.seriesName : l.push(t.seriesName), l.forEach((function (t) { e.series.forEach((function (e, o) { if (e.name === t) { var l = o; a === o || r ? !r || s.indexOf(o) > -1 ? i[a].push([a, o]) : console.warn("Series '" + e.name + "' referenced more than once in what looks like the new style. That is, when using either seriesName: [], or when there are more series than yaxes.") : (i[o].push([o, a]), l = a), n = !0, -1 !== (l = s.indexOf(l)) && s.splice(l, 1) } })) })) } n || o.push(a) })), i = i.map((function (t, e) { var i = []; return t.forEach((function (t) { a[t[1]] = t[0], i.push(t[1]) })), i })); for (var n = e.yaxis.length - 1, l = 0; l < o.length && (n = o[l], i[n] = [], s); l++) { var h = s[0]; s.shift(), i[n].push(h), a[h] = n } s.forEach((function (t) { i[n].push(t), a[t] = n })), t.seriesYAxisMap = i.map((function (t) { return t })), t.seriesYAxisReverseMap = a.map((function (t) { return t })) } }, { key: "scaleMultipleYAxes", value: function () { var t = this, e = this.w.config, i = this.w.globals; this.setSeriesYAxisMappings(); var a = i.seriesYAxisMap, s = i.minYArr, r = i.maxYArr; i.allSeriesCollapsed = !0, i.barGroups = [], a.forEach((function (a, o) { var n = []; a.forEach((function (t) { var i = e.series[t].group; n.indexOf(i) < 0 && n.push(i) })), a.length > 0 ? function () { var l, h, c = Number.MAX_VALUE, d = -Number.MAX_VALUE, g = c, u = d; if (e.chart.stacked) !function () { var t = i.seriesX[a[0]], s = [], r = [], p = []; n.forEach((function () { s.push(t.map((function () { return Number.MIN_VALUE }))), r.push(t.map((function () { return Number.MIN_VALUE }))), p.push(t.map((function () { return Number.MIN_VALUE }))) })); for (var f = function (t) { !l && e.series[a[t]].type && (l = e.series[a[t]].type); var c = a[t]; h = e.series[c].group ? e.series[c].group : "axis-".concat(o), !(i.collapsedSeriesIndices.indexOf(c) < 0 && i.ancillaryCollapsedSeriesIndices.indexOf(c) < 0) || (i.allSeriesCollapsed = !1, n.forEach((function (t, a) { if (e.series[c].group === t) for (var o = 0; o < i.series[c].length; o++) { var n = i.series[c][o]; n >= 0 ? r[a][o] += n : p[a][o] += n, s[a][o] += n, g = Math.min(g, n), u = Math.max(u, n) } }))), "bar" !== l && "column" !== l || i.barGroups.push(h) }, x = 0; x < a.length; x++)f(x); l || (l = e.chart.type), "bar" === l || "column" === l ? n.forEach((function (t, e) { c = Math.min(c, Math.min.apply(null, p[e])), d = Math.max(d, Math.max.apply(null, r[e])) })) : (n.forEach((function (t, e) { g = Math.min(g, Math.min.apply(null, s[e])), u = Math.max(u, Math.max.apply(null, s[e])) })), c = g, d = u), c === Number.MIN_VALUE && d === Number.MIN_VALUE && (d = -Number.MAX_VALUE) }(); else for (var p = 0; p < a.length; p++) { var f = a[p]; c = Math.min(c, s[f]), d = Math.max(d, r[f]), !(i.collapsedSeriesIndices.indexOf(f) < 0 && i.ancillaryCollapsedSeriesIndices.indexOf(f) < 0) || (i.allSeriesCollapsed = !1) } void 0 !== e.yaxis[o].min && (c = "function" == typeof e.yaxis[o].min ? e.yaxis[o].min(c) : e.yaxis[o].min), void 0 !== e.yaxis[o].max && (d = "function" == typeof e.yaxis[o].max ? e.yaxis[o].max(d) : e.yaxis[o].max), i.barGroups = i.barGroups.filter((function (t, e, i) { return i.indexOf(t) === e })), t.setYScaleForIndex(o, c, d), a.forEach((function (t) { s[t] = i.yAxisScale[o].niceMin, r[t] = i.yAxisScale[o].niceMax })) }() : t.setYScaleForIndex(o, 0, -Number.MAX_VALUE) })) } }]), t }(), U = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w, this.scales = new _(e) } return r(t, [{ key: "init", value: function () { this.setYRange(), this.setXRange(), this.setZRange() } }, { key: "getMinYMaxY", value: function (t) { var e = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : Number.MAX_VALUE, i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : -Number.MAX_VALUE, a = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : null, s = this.w.config, r = this.w.globals, o = -Number.MAX_VALUE, n = Number.MIN_VALUE; null === a && (a = t + 1); var l = 0, h = 0, c = void 0; if (r.seriesX.length >= a) { var d, g; l = 0, h = (c = u(new Set((d = []).concat.apply(d, u(r.seriesX.slice(t, a)))))).length - 1; var p = null === (g = r.brushSource) || void 0 === g ? void 0 : g.w.config.chart.brush; if (s.chart.zoom.enabled && s.chart.zoom.autoScaleYaxis || null != p && p.enabled && null != p && p.autoScaleYaxis) { if (s.xaxis.min) for (l = 0; l < h && c[l] < s.xaxis.min; l++); if (s.xaxis.max) for (; h > l && c[h] > s.xaxis.max; h--); } } var f = r.series, b = f, v = f; "candlestick" === s.chart.type ? (b = r.seriesCandleL, v = r.seriesCandleH) : "boxPlot" === s.chart.type ? (b = r.seriesCandleO, v = r.seriesCandleC) : r.isRangeData && (b = r.seriesRangeStart, v = r.seriesRangeEnd); for (var m = t; m < a; m++) { r.dataPoints = Math.max(r.dataPoints, f[m].length); var y = s.series[m].type; r.categoryLabels.length && (r.dataPoints = r.categoryLabels.filter((function (t) { return void 0 !== t })).length), r.labels.length && "datetime" !== s.xaxis.type && 0 !== r.series.reduce((function (t, e) { return t + e.length }), 0) && (r.dataPoints = Math.max(r.dataPoints, r.labels.length)), c || (l = 0, h = r.series[m].length); for (var w = l; w <= h && w < r.series[m].length; w++) { var k = f[m][w]; if (null !== k && x.isNumber(k)) { switch (void 0 !== v[m][w] && (o = Math.max(o, v[m][w]), e = Math.min(e, v[m][w])), void 0 !== b[m][w] && (e = Math.min(e, b[m][w]), i = Math.max(i, b[m][w])), y) { case "candlestick": void 0 !== r.seriesCandleC[m][w] && (o = Math.max(o, r.seriesCandleH[m][w]), e = Math.min(e, r.seriesCandleL[m][w])); break; case "boxPlot": void 0 !== r.seriesCandleC[m][w] && (o = Math.max(o, r.seriesCandleC[m][w]), e = Math.min(e, r.seriesCandleO[m][w])) }y && "candlestick" !== y && "boxPlot" !== y && "rangeArea" !== y && "rangeBar" !== y && (o = Math.max(o, r.series[m][w]), e = Math.min(e, r.series[m][w])), i = o, r.seriesGoals[m] && r.seriesGoals[m][w] && Array.isArray(r.seriesGoals[m][w]) && r.seriesGoals[m][w].forEach((function (t) { n !== Number.MIN_VALUE && (n = Math.min(n, t.value), e = n), o = Math.max(o, t.value), i = o })), x.isFloat(k) && (k = x.noExponents(k), r.yValueDecimal = Math.max(r.yValueDecimal, k.toString().split(".")[1].length)), n > b[m][w] && b[m][w] < 0 && (n = b[m][w]) } else r.hasNullValues = !0 } "bar" !== y && "column" !== y || (n < 0 && o < 0 && (o = 0, i = Math.max(i, 0)), n === Number.MIN_VALUE && (n = 0, e = Math.min(e, 0))) } return "rangeBar" === s.chart.type && r.seriesRangeStart.length && r.isBarHorizontal && (n = e), "bar" === s.chart.type && (n < 0 && o < 0 && (o = 0), n === Number.MIN_VALUE && (n = 0)), { minY: n, maxY: o, lowestY: e, highestY: i } } }, { key: "setYRange", value: function () { var t = this.w.globals, e = this.w.config; t.maxY = -Number.MAX_VALUE, t.minY = Number.MIN_VALUE; var i, a = Number.MAX_VALUE; if (t.isMultipleYAxis) { a = Number.MAX_VALUE; for (var s = 0; s < t.series.length; s++)i = this.getMinYMaxY(s), t.minYArr[s] = i.lowestY, t.maxYArr[s] = i.highestY, a = Math.min(a, i.lowestY) } if (i = this.getMinYMaxY(0, a, null, t.series.length), "bar" === e.chart.type ? (t.minY = i.minY, t.maxY = i.maxY) : (t.minY = i.lowestY, t.maxY = i.highestY), a = i.lowestY, e.chart.stacked && this._setStackedMinMax(), "line" === e.chart.type || "area" === e.chart.type || "scatter" === e.chart.type || "candlestick" === e.chart.type || "boxPlot" === e.chart.type || "rangeBar" === e.chart.type && !t.isBarHorizontal ? t.minY === Number.MIN_VALUE && a !== -Number.MAX_VALUE && a !== t.maxY && (t.minY = a) : t.minY = i.minY, e.yaxis.forEach((function (e, i) { void 0 !== e.max && ("number" == typeof e.max ? t.maxYArr[i] = e.max : "function" == typeof e.max && (t.maxYArr[i] = e.max(t.isMultipleYAxis ? t.maxYArr[i] : t.maxY)), t.maxY = t.maxYArr[i]), void 0 !== e.min && ("number" == typeof e.min ? t.minYArr[i] = e.min : "function" == typeof e.min && (t.minYArr[i] = e.min(t.isMultipleYAxis ? t.minYArr[i] === Number.MIN_VALUE ? 0 : t.minYArr[i] : t.minY)), t.minY = t.minYArr[i]) })), t.isBarHorizontal) { ["min", "max"].forEach((function (i) { void 0 !== e.xaxis[i] && "number" == typeof e.xaxis[i] && ("min" === i ? t.minY = e.xaxis[i] : t.maxY = e.xaxis[i]) })) } return t.isMultipleYAxis ? (this.scales.scaleMultipleYAxes(), t.minY = a) : (this.scales.setYScaleForIndex(0, t.minY, t.maxY), t.minY = t.yAxisScale[0].niceMin, t.maxY = t.yAxisScale[0].niceMax, t.minYArr[0] = t.minY, t.maxYArr[0] = t.maxY), t.barGroups = [], t.lineGroups = [], t.areaGroups = [], e.series.forEach((function (i) { switch (i.type || e.chart.type) { case "bar": case "column": t.barGroups.push(i.group); break; case "line": t.lineGroups.push(i.group); break; case "area": t.areaGroups.push(i.group) } })), t.barGroups = t.barGroups.filter((function (t, e, i) { return i.indexOf(t) === e })), t.lineGroups = t.lineGroups.filter((function (t, e, i) { return i.indexOf(t) === e })), t.areaGroups = t.areaGroups.filter((function (t, e, i) { return i.indexOf(t) === e })), { minY: t.minY, maxY: t.maxY, minYArr: t.minYArr, maxYArr: t.maxYArr, yAxisScale: t.yAxisScale } } }, { key: "setXRange", value: function () { var t = this.w.globals, e = this.w.config, i = "numeric" === e.xaxis.type || "datetime" === e.xaxis.type || "category" === e.xaxis.type && !t.noLabelsProvided || t.noLabelsProvided || t.isXNumeric; if (t.isXNumeric && function () { for (var e = 0; e < t.series.length; e++)if (t.labels[e]) for (var i = 0; i < t.labels[e].length; i++)null !== t.labels[e][i] && x.isNumber(t.labels[e][i]) && (t.maxX = Math.max(t.maxX, t.labels[e][i]), t.initialMaxX = Math.max(t.maxX, t.labels[e][i]), t.minX = Math.min(t.minX, t.labels[e][i]), t.initialMinX = Math.min(t.minX, t.labels[e][i])) }(), t.noLabelsProvided && 0 === e.xaxis.categories.length && (t.maxX = t.labels[t.labels.length - 1], t.initialMaxX = t.labels[t.labels.length - 1], t.minX = 1, t.initialMinX = 1), t.isXNumeric || t.noLabelsProvided || t.dataFormatXNumeric) { var a; if (void 0 === e.xaxis.tickAmount ? (a = Math.round(t.svgWidth / 150), "numeric" === e.xaxis.type && t.dataPoints < 30 && (a = t.dataPoints - 1), a > t.dataPoints && 0 !== t.dataPoints && (a = t.dataPoints - 1)) : "dataPoints" === e.xaxis.tickAmount ? (t.series.length > 1 && (a = t.series[t.maxValsInArrayIndex].length - 1), t.isXNumeric && (a = t.maxX - t.minX - 1)) : a = e.xaxis.tickAmount, t.xTickAmount = a, void 0 !== e.xaxis.max && "number" == typeof e.xaxis.max && (t.maxX = e.xaxis.max), void 0 !== e.xaxis.min && "number" == typeof e.xaxis.min && (t.minX = e.xaxis.min), void 0 !== e.xaxis.range && (t.minX = t.maxX - e.xaxis.range), t.minX !== Number.MAX_VALUE && t.maxX !== -Number.MAX_VALUE) if (e.xaxis.convertedCatToNumeric && !t.dataFormatXNumeric) { for (var s = [], r = t.minX - 1; r < t.maxX; r++)s.push(r + 1); t.xAxisScale = { result: s, niceMin: s[0], niceMax: s[s.length - 1] } } else t.xAxisScale = this.scales.setXScale(t.minX, t.maxX); else t.xAxisScale = this.scales.linearScale(0, a, a, 0, e.xaxis.stepSize), t.noLabelsProvided && t.labels.length > 0 && (t.xAxisScale = this.scales.linearScale(1, t.labels.length, a - 1, 0, e.xaxis.stepSize), t.seriesX = t.labels.slice()); i && (t.labels = t.xAxisScale.result.slice()) } return t.isBarHorizontal && t.labels.length && (t.xTickAmount = t.labels.length), this._handleSingleDataPoint(), this._getMinXDiff(), { minX: t.minX, maxX: t.maxX } } }, { key: "setZRange", value: function () { var t = this.w.globals; if (t.isDataXYZ) for (var e = 0; e < t.series.length; e++)if (void 0 !== t.seriesZ[e]) for (var i = 0; i < t.seriesZ[e].length; i++)null !== t.seriesZ[e][i] && x.isNumber(t.seriesZ[e][i]) && (t.maxZ = Math.max(t.maxZ, t.seriesZ[e][i]), t.minZ = Math.min(t.minZ, t.seriesZ[e][i])) } }, { key: "_handleSingleDataPoint", value: function () { var t = this.w.globals, e = this.w.config; if (t.minX === t.maxX) { var i = new A(this.ctx); if ("datetime" === e.xaxis.type) { var a = i.getDate(t.minX); e.xaxis.labels.datetimeUTC ? a.setUTCDate(a.getUTCDate() - 2) : a.setDate(a.getDate() - 2), t.minX = new Date(a).getTime(); var s = i.getDate(t.maxX); e.xaxis.labels.datetimeUTC ? s.setUTCDate(s.getUTCDate() + 2) : s.setDate(s.getDate() + 2), t.maxX = new Date(s).getTime() } else ("numeric" === e.xaxis.type || "category" === e.xaxis.type && !t.noLabelsProvided) && (t.minX = t.minX - 2, t.initialMinX = t.minX, t.maxX = t.maxX + 2, t.initialMaxX = t.maxX) } } }, { key: "_getMinXDiff", value: function () { var t = this.w.globals; t.isXNumeric && t.seriesX.forEach((function (e, i) { 1 === e.length && e.push(t.seriesX[t.maxValsInArrayIndex][t.seriesX[t.maxValsInArrayIndex].length - 1]); var a = e.slice(); a.sort((function (t, e) { return t - e })), a.forEach((function (e, i) { if (i > 0) { var s = e - a[i - 1]; s > 0 && (t.minXDiff = Math.min(s, t.minXDiff)) } })), 1 !== t.dataPoints && t.minXDiff !== Number.MAX_VALUE || (t.minXDiff = .5) })) } }, { key: "_setStackedMinMax", value: function () { var t = this, e = this.w.globals; if (e.series.length) { var i = e.seriesGroups; i.length || (i = [this.w.globals.seriesNames.map((function (t) { return t }))]); var a = {}, s = {}; i.forEach((function (i) { a[i] = [], s[i] = [], t.w.config.series.map((function (t, a) { return i.indexOf(e.seriesNames[a]) > -1 ? a : null })).filter((function (t) { return null !== t })).forEach((function (r) { for (var o = 0; o < e.series[e.maxValsInArrayIndex].length; o++) { var n, l, h, c; void 0 === a[i][o] && (a[i][o] = 0, s[i][o] = 0), (t.w.config.chart.stacked && !e.comboCharts || t.w.config.chart.stacked && e.comboCharts && (!t.w.config.chart.stackOnlyBar || "bar" === (null === (n = t.w.config.series) || void 0 === n || null === (l = n[r]) || void 0 === l ? void 0 : l.type) || "column" === (null === (h = t.w.config.series) || void 0 === h || null === (c = h[r]) || void 0 === c ? void 0 : c.type))) && null !== e.series[r][o] && x.isNumber(e.series[r][o]) && (e.series[r][o] > 0 ? a[i][o] += parseFloat(e.series[r][o]) + 1e-4 : s[i][o] += parseFloat(e.series[r][o])) } })) })), Object.entries(a).forEach((function (t) { var i = g(t, 1)[0]; a[i].forEach((function (t, r) { e.maxY = Math.max(e.maxY, a[i][r]), e.minY = Math.min(e.minY, s[i][r]) })) })) } } }]), t }(), q = function () { function t(e, i) { a(this, t), this.ctx = e, this.elgrid = i, this.w = e.w; var s = this.w; this.xaxisFontSize = s.config.xaxis.labels.style.fontSize, this.axisFontFamily = s.config.xaxis.labels.style.fontFamily, this.xaxisForeColors = s.config.xaxis.labels.style.colors, this.isCategoryBarHorizontal = "bar" === s.config.chart.type && s.config.plotOptions.bar.horizontal, this.xAxisoffX = 0, "bottom" === s.config.xaxis.position && (this.xAxisoffX = s.globals.gridHeight), this.drawnLabels = [], this.axesUtils = new C(e) } return r(t, [{ key: "drawYaxis", value: function (t) { var e = this, i = this.w, a = new m(this.ctx), s = i.config.yaxis[t].labels.style, r = s.fontSize, o = s.fontFamily, n = s.fontWeight, l = a.group({ class: "apexcharts-yaxis", rel: t, transform: "translate(" + i.globals.translateYAxisX[t] + ", 0)" }); if (this.axesUtils.isYAxisHidden(t)) return l; var h = a.group({ class: "apexcharts-yaxis-texts-g" }); l.add(h); var c = i.globals.yAxisScale[t].result.length - 1, d = i.globals.gridHeight / c, g = i.globals.yLabelFormatters[t], u = i.globals.yAxisScale[t].result.slice(); u = this.axesUtils.checkForReversedLabels(t, u); var p = ""; if (i.config.yaxis[t].labels.show) { var f = i.globals.translateY + i.config.yaxis[t].labels.offsetY; i.globals.isBarHorizontal ? f = 0 : "heatmap" === i.config.chart.type && (f -= d / 2), f += parseInt(i.config.yaxis[t].labels.style.fontSize, 10) / 3; for (var x = function (l) { var x = u[l]; x = g(x, l, i); var b = i.config.yaxis[t].labels.padding; i.config.yaxis[t].opposite && 0 !== i.config.yaxis.length && (b *= -1); var v = "end"; i.config.yaxis[t].opposite && (v = "start"), "left" === i.config.yaxis[t].labels.align ? v = "start" : "center" === i.config.yaxis[t].labels.align ? v = "middle" : "right" === i.config.yaxis[t].labels.align && (v = "end"); var m = e.axesUtils.getYAxisForeColor(s.colors, t), y = a.drawText({ x: b, y: f, text: x, textAnchor: v, fontSize: r, fontFamily: o, fontWeight: n, maxWidth: i.config.yaxis[t].labels.maxWidth, foreColor: Array.isArray(m) ? m[l] : m, isPlainText: !1, cssClass: "apexcharts-yaxis-label " + s.cssClass }); l === c && (p = y), h.add(y); var w = document.createElementNS(i.globals.SVGNS, "title"); if (w.textContent = Array.isArray(x) ? x.join(" ") : x, y.node.appendChild(w), 0 !== i.config.yaxis[t].labels.rotate) { var k = a.rotateAroundCenter(p.node), A = a.rotateAroundCenter(y.node); y.node.setAttribute("transform", "rotate(".concat(i.config.yaxis[t].labels.rotate, " ").concat(k.x, " ").concat(A.y, ")")) } f += d }, b = c; b >= 0; b--)x(b) } if (void 0 !== i.config.yaxis[t].title.text) { var v = a.group({ class: "apexcharts-yaxis-title" }), y = 0; i.config.yaxis[t].opposite && (y = i.globals.translateYAxisX[t]); var w = a.drawText({ x: y, y: i.globals.gridHeight / 2 + i.globals.translateY + i.config.yaxis[t].title.offsetY, text: i.config.yaxis[t].title.text, textAnchor: "end", foreColor: i.config.yaxis[t].title.style.color, fontSize: i.config.yaxis[t].title.style.fontSize, fontWeight: i.config.yaxis[t].title.style.fontWeight, fontFamily: i.config.yaxis[t].title.style.fontFamily, cssClass: "apexcharts-yaxis-title-text " + i.config.yaxis[t].title.style.cssClass }); v.add(w), l.add(v) } var k = i.config.yaxis[t].axisBorder, A = 31 + k.offsetX; if (i.config.yaxis[t].opposite && (A = -31 - k.offsetX), k.show) { var S = a.drawLine(A, i.globals.translateY + k.offsetY - 2, A, i.globals.gridHeight + i.globals.translateY + k.offsetY + 2, k.color, 0, k.width); l.add(S) } return i.config.yaxis[t].axisTicks.show && this.axesUtils.drawYAxisTicks(A, c, k, i.config.yaxis[t].axisTicks, t, d, l), l } }, { key: "drawYaxisInversed", value: function (t) { var e = this.w, i = new m(this.ctx), a = i.group({ class: "apexcharts-xaxis apexcharts-yaxis-inversed" }), s = i.group({ class: "apexcharts-xaxis-texts-g", transform: "translate(".concat(e.globals.translateXAxisX, ", ").concat(e.globals.translateXAxisY, ")") }); a.add(s); var r = e.globals.yAxisScale[t].result.length - 1, o = e.globals.gridWidth / r + .1, n = o + e.config.xaxis.labels.offsetX, l = e.globals.xLabelFormatter, h = e.globals.yAxisScale[t].result.slice(), c = e.globals.timescaleLabels; c.length > 0 && (this.xaxisLabels = c.slice(), r = (h = c.slice()).length), h = this.axesUtils.checkForReversedLabels(t, h); var d = c.length; if (e.config.xaxis.labels.show) for (var g = d ? 0 : r; d ? g < d : g >= 0; d ? g++ : g--) { var u = h[g]; u = l(u, g, e); var p = e.globals.gridWidth + e.globals.padHorizontal - (n - o + e.config.xaxis.labels.offsetX); if (c.length) { var f = this.axesUtils.getLabel(h, c, p, g, this.drawnLabels, this.xaxisFontSize); p = f.x, u = f.text, this.drawnLabels.push(f.text), 0 === g && e.globals.skipFirstTimelinelabel && (u = ""), g === h.length - 1 && e.globals.skipLastTimelinelabel && (u = "") } var x = i.drawText({ x: p, y: this.xAxisoffX + e.config.xaxis.labels.offsetY + 30 - ("top" === e.config.xaxis.position ? e.globals.xAxisHeight + e.config.xaxis.axisTicks.height - 2 : 0), text: u, textAnchor: "middle", foreColor: Array.isArray(this.xaxisForeColors) ? this.xaxisForeColors[t] : this.xaxisForeColors, fontSize: this.xaxisFontSize, fontFamily: this.xaxisFontFamily, fontWeight: e.config.xaxis.labels.style.fontWeight, isPlainText: !1, cssClass: "apexcharts-xaxis-label " + e.config.xaxis.labels.style.cssClass }); s.add(x), x.tspan(u); var b = document.createElementNS(e.globals.SVGNS, "title"); b.textContent = u, x.node.appendChild(b), n += o } return this.inversedYAxisTitleText(a), this.inversedYAxisBorder(a), a } }, { key: "inversedYAxisBorder", value: function (t) { var e = this.w, i = new m(this.ctx), a = e.config.xaxis.axisBorder; if (a.show) { var s = 0; "bar" === e.config.chart.type && e.globals.isXNumeric && (s -= 15); var r = i.drawLine(e.globals.padHorizontal + s + a.offsetX, this.xAxisoffX, e.globals.gridWidth, this.xAxisoffX, a.color, 0, a.height); this.elgrid && this.elgrid.elGridBorders && e.config.grid.show ? this.elgrid.elGridBorders.add(r) : t.add(r) } } }, { key: "inversedYAxisTitleText", value: function (t) { var e = this.w, i = new m(this.ctx); if (void 0 !== e.config.xaxis.title.text) { var a = i.group({ class: "apexcharts-xaxis-title apexcharts-yaxis-title-inversed" }), s = i.drawText({ x: e.globals.gridWidth / 2 + e.config.xaxis.title.offsetX, y: this.xAxisoffX + parseFloat(this.xaxisFontSize) + parseFloat(e.config.xaxis.title.style.fontSize) + e.config.xaxis.title.offsetY + 20, text: e.config.xaxis.title.text, textAnchor: "middle", fontSize: e.config.xaxis.title.style.fontSize, fontFamily: e.config.xaxis.title.style.fontFamily, fontWeight: e.config.xaxis.title.style.fontWeight, foreColor: e.config.xaxis.title.style.color, cssClass: "apexcharts-xaxis-title-text " + e.config.xaxis.title.style.cssClass }); a.add(s), t.add(a) } } }, { key: "yAxisTitleRotate", value: function (t, e) { var i = this.w, a = new m(this.ctx), s = { width: 0, height: 0 }, r = { width: 0, height: 0 }, o = i.globals.dom.baseEl.querySelector(" .apexcharts-yaxis[rel='".concat(t, "'] .apexcharts-yaxis-texts-g")); null !== o && (s = o.getBoundingClientRect()); var n = i.globals.dom.baseEl.querySelector(".apexcharts-yaxis[rel='".concat(t, "'] .apexcharts-yaxis-title text")); if (null !== n && (r = n.getBoundingClientRect()), null !== n) { var l = this.xPaddingForYAxisTitle(t, s, r, e); n.setAttribute("x", l.xPos - (e ? 10 : 0)) } if (null !== n) { var h = a.rotateAroundCenter(n); n.setAttribute("transform", "rotate(".concat(e ? -1 * i.config.yaxis[t].title.rotate : i.config.yaxis[t].title.rotate, " ").concat(h.x, " ").concat(h.y, ")")) } } }, { key: "xPaddingForYAxisTitle", value: function (t, e, i, a) { var s = this.w, r = 0, o = 0, n = 10; return void 0 === s.config.yaxis[t].title.text || t < 0 ? { xPos: o, padd: 0 } : (a ? (o = e.width + s.config.yaxis[t].title.offsetX + i.width / 2 + n / 2, 0 === (r += 1) && (o -= n / 2)) : (o = -1 * e.width + s.config.yaxis[t].title.offsetX + n / 2 + i.width / 2, s.globals.isBarHorizontal && (n = 25, o = -1 * e.width - s.config.yaxis[t].title.offsetX - n)), { xPos: o, padd: n }) } }, { key: "setYAxisXPosition", value: function (t, e) { var i = this.w, a = 0, s = 0, r = 18, o = 1; i.config.yaxis.length > 1 && (this.multipleYs = !0), i.config.yaxis.map((function (n, l) { var h = i.globals.ignoreYAxisIndexes.indexOf(l) > -1 || !n.show || n.floating || 0 === t[l].width, c = t[l].width + e[l].width; n.opposite ? i.globals.isBarHorizontal ? (s = i.globals.gridWidth + i.globals.translateX - 1, i.globals.translateYAxisX[l] = s - n.labels.offsetX) : (s = i.globals.gridWidth + i.globals.translateX + o, h || (o = o + c + 20), i.globals.translateYAxisX[l] = s - n.labels.offsetX + 20) : (a = i.globals.translateX - r, h || (r = r + c + 20), i.globals.translateYAxisX[l] = a + n.labels.offsetX) })) } }, { key: "setYAxisTextAlignments", value: function () { var t = this.w, e = t.globals.dom.baseEl.getElementsByClassName("apexcharts-yaxis"); (e = x.listToArray(e)).forEach((function (e, i) { var a = t.config.yaxis[i]; if (a && !a.floating && void 0 !== a.labels.align) { var s = t.globals.dom.baseEl.querySelector(".apexcharts-yaxis[rel='".concat(i, "'] .apexcharts-yaxis-texts-g")), r = t.globals.dom.baseEl.querySelectorAll(".apexcharts-yaxis[rel='".concat(i, "'] .apexcharts-yaxis-label")); r = x.listToArray(r); var o = s.getBoundingClientRect(); "left" === a.labels.align ? (r.forEach((function (t, e) { t.setAttribute("text-anchor", "start") })), a.opposite || s.setAttribute("transform", "translate(-".concat(o.width, ", 0)"))) : "center" === a.labels.align ? (r.forEach((function (t, e) { t.setAttribute("text-anchor", "middle") })), s.setAttribute("transform", "translate(".concat(o.width / 2 * (a.opposite ? 1 : -1), ", 0)"))) : "right" === a.labels.align && (r.forEach((function (t, e) { t.setAttribute("text-anchor", "end") })), a.opposite && s.setAttribute("transform", "translate(".concat(o.width, ", 0)"))) } })) } }]), t }(), Z = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w, this.documentEvent = x.bind(this.documentEvent, this) } return r(t, [{ key: "addEventListener", value: function (t, e) { var i = this.w; i.globals.events.hasOwnProperty(t) ? i.globals.events[t].push(e) : i.globals.events[t] = [e] } }, { key: "removeEventListener", value: function (t, e) { var i = this.w; if (i.globals.events.hasOwnProperty(t)) { var a = i.globals.events[t].indexOf(e); -1 !== a && i.globals.events[t].splice(a, 1) } } }, { key: "fireEvent", value: function (t, e) { var i = this.w; if (i.globals.events.hasOwnProperty(t)) { e && e.length || (e = []); for (var a = i.globals.events[t], s = a.length, r = 0; r < s; r++)a[r].apply(null, e) } } }, { key: "setupEventHandlers", value: function () { var t = this, e = this.w, i = this.ctx, a = e.globals.dom.baseEl.querySelector(e.globals.chartClass); this.ctx.eventList.forEach((function (t) { a.addEventListener(t, (function (t) { var a = Object.assign({}, e, { seriesIndex: e.globals.axisCharts ? e.globals.capturedSeriesIndex : 0, dataPointIndex: e.globals.capturedDataPointIndex }); "mousemove" === t.type || "touchmove" === t.type ? "function" == typeof e.config.chart.events.mouseMove && e.config.chart.events.mouseMove(t, i, a) : "mouseleave" === t.type || "touchleave" === t.type ? "function" == typeof e.config.chart.events.mouseLeave && e.config.chart.events.mouseLeave(t, i, a) : ("mouseup" === t.type && 1 === t.which || "touchend" === t.type) && ("function" == typeof e.config.chart.events.click && e.config.chart.events.click(t, i, a), i.ctx.events.fireEvent("click", [t, i, a])) }), { capture: !1, passive: !0 }) })), this.ctx.eventList.forEach((function (i) { e.globals.dom.baseEl.addEventListener(i, t.documentEvent, { passive: !0 }) })), this.ctx.core.setupBrushHandler() } }, { key: "documentEvent", value: function (t) { var e = this.w, i = t.target.className; if ("click" === t.type) { var a = e.globals.dom.baseEl.querySelector(".apexcharts-menu"); a && a.classList.contains("apexcharts-menu-open") && "apexcharts-menu-icon" !== i && a.classList.remove("apexcharts-menu-open") } e.globals.clientX = "touchmove" === t.type ? t.touches[0].clientX : t.clientX, e.globals.clientY = "touchmove" === t.type ? t.touches[0].clientY : t.clientY } }]), t }(), $ = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w } return r(t, [{ key: "setCurrentLocaleValues", value: function (t) { var e = this.w.config.chart.locales; window.Apex.chart && window.Apex.chart.locales && window.Apex.chart.locales.length > 0 && (e = this.w.config.chart.locales.concat(window.Apex.chart.locales)); var i = e.filter((function (e) { return e.name === t }))[0]; if (!i) throw new Error("Wrong locale name provided. Please make sure you set the correct locale name in options"); var a = x.extend(M, i); this.w.globals.locale = a.options } }]), t }(), J = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w } return r(t, [{ key: "drawAxis", value: function (t, e) { var i, a, s = this, r = this.w.globals, o = this.w.config, n = new V(this.ctx, e), l = new q(this.ctx, e); r.axisCharts && "radar" !== t && (r.isBarHorizontal ? (a = l.drawYaxisInversed(0), i = n.drawXaxisInversed(0), r.dom.elGraphical.add(i), r.dom.elGraphical.add(a)) : (i = n.drawXaxis(), r.dom.elGraphical.add(i), o.yaxis.map((function (t, e) { if (-1 === r.ignoreYAxisIndexes.indexOf(e) && (a = l.drawYaxis(e), r.dom.Paper.add(a), "back" === s.w.config.grid.position)) { var i = r.dom.Paper.children()[1]; i.remove(), r.dom.Paper.add(i) } })))) } }]), t }(), Q = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w } return r(t, [{ key: "drawXCrosshairs", value: function () { var t = this.w, e = new m(this.ctx), i = new v(this.ctx), a = t.config.xaxis.crosshairs.fill.gradient, s = t.config.xaxis.crosshairs.dropShadow, r = t.config.xaxis.crosshairs.fill.type, o = a.colorFrom, n = a.colorTo, l = a.opacityFrom, h = a.opacityTo, c = a.stops, d = s.enabled, g = s.left, u = s.top, p = s.blur, f = s.color, b = s.opacity, y = t.config.xaxis.crosshairs.fill.color; if (t.config.xaxis.crosshairs.show) { "gradient" === r && (y = e.drawGradient("vertical", o, n, l, h, null, c, null)); var w = e.drawRect(); 1 === t.config.xaxis.crosshairs.width && (w = e.drawLine()); var k = t.globals.gridHeight; (!x.isNumber(k) || k < 0) && (k = 0); var A = t.config.xaxis.crosshairs.width; (!x.isNumber(A) || A < 0) && (A = 0), w.attr({ class: "apexcharts-xcrosshairs", x: 0, y: 0, y2: k, width: A, height: k, fill: y, filter: "none", "fill-opacity": t.config.xaxis.crosshairs.opacity, stroke: t.config.xaxis.crosshairs.stroke.color, "stroke-width": t.config.xaxis.crosshairs.stroke.width, "stroke-dasharray": t.config.xaxis.crosshairs.stroke.dashArray }), d && (w = i.dropShadow(w, { left: g, top: u, blur: p, color: f, opacity: b })), t.globals.dom.elGraphical.add(w) } } }, { key: "drawYCrosshairs", value: function () { var t = this.w, e = new m(this.ctx), i = t.config.yaxis[0].crosshairs, a = t.globals.barPadForNumericAxis; if (t.config.yaxis[0].crosshairs.show) { var s = e.drawLine(-a, 0, t.globals.gridWidth + a, 0, i.stroke.color, i.stroke.dashArray, i.stroke.width); s.attr({ class: "apexcharts-ycrosshairs" }), t.globals.dom.elGraphical.add(s) } var r = e.drawLine(-a, 0, t.globals.gridWidth + a, 0, i.stroke.color, 0, 0); r.attr({ class: "apexcharts-ycrosshairs-hidden" }), t.globals.dom.elGraphical.add(r) } }]), t }(), K = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w } return r(t, [{ key: "checkResponsiveConfig", value: function (t) { var e = this, i = this.w, a = i.config; if (0 !== a.responsive.length) { var s = a.responsive.slice(); s.sort((function (t, e) { return t.breakpoint > e.breakpoint ? 1 : e.breakpoint > t.breakpoint ? -1 : 0 })).reverse(); var r = new Y({}), o = function () { var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, a = s[0].breakpoint, o = window.innerWidth > 0 ? window.innerWidth : screen.width; if (o > a) { var n = x.clone(i.globals.initialConfig); n.series = x.clone(i.config.series); var l = y.extendArrayProps(r, n, i); t = x.extend(l, t), t = x.extend(i.config, t), e.overrideResponsiveOptions(t) } else for (var h = 0; h < s.length; h++)if (o < s[h].breakpoint) { var c = y.extendArrayProps(r, s[h].options, i); t = x.extend(c, t), t = x.extend(i.config, t), e.overrideResponsiveOptions(t) } }; if (t) { var n = y.extendArrayProps(r, t, i); n = x.extend(i.config, n), o(n = x.extend(n, t)) } else o({}) } } }, { key: "overrideResponsiveOptions", value: function (t) { var e = new Y(t).init({ responsiveOverride: !0 }); this.w.config = e } }]), t }(), tt = function () { function t(e) { a(this, t), this.ctx = e, this.colors = [], this.w = e.w; var i = this.w; this.isColorFn = !1, this.isHeatmapDistributed = "treemap" === i.config.chart.type && i.config.plotOptions.treemap.distributed || "heatmap" === i.config.chart.type && i.config.plotOptions.heatmap.distributed, this.isBarDistributed = i.config.plotOptions.bar.distributed && ("bar" === i.config.chart.type || "rangeBar" === i.config.chart.type) } return r(t, [{ key: "init", value: function () { this.setDefaultColors() } }, { key: "setDefaultColors", value: function () { var t, e = this, i = this.w, a = new x; if (i.globals.dom.elWrap.classList.add("apexcharts-theme-".concat(i.config.theme.mode)), void 0 === i.config.colors || 0 === (null === (t = i.config.colors) || void 0 === t ? void 0 : t.length) ? i.globals.colors = this.predefined() : (i.globals.colors = i.config.colors, Array.isArray(i.config.colors) && i.config.colors.length > 0 && "function" == typeof i.config.colors[0] && (i.globals.colors = i.config.series.map((function (t, a) { var s = i.config.colors[a]; return s || (s = i.config.colors[0]), "function" == typeof s ? (e.isColorFn = !0, s({ value: i.globals.axisCharts ? i.globals.series[a][0] ? i.globals.series[a][0] : 0 : i.globals.series[a], seriesIndex: a, dataPointIndex: a, w: i })) : s })))), i.globals.seriesColors.map((function (t, e) { t && (i.globals.colors[e] = t) })), i.config.theme.monochrome.enabled) { var s = [], r = i.globals.series.length; (this.isBarDistributed || this.isHeatmapDistributed) && (r = i.globals.series[0].length * i.globals.series.length); for (var o = i.config.theme.monochrome.color, n = 1 / (r / i.config.theme.monochrome.shadeIntensity), l = i.config.theme.monochrome.shadeTo, h = 0, c = 0; c < r; c++) { var d = void 0; "dark" === l ? (d = a.shadeColor(-1 * h, o), h += n) : (d = a.shadeColor(h, o), h += n), s.push(d) } i.globals.colors = s.slice() } var g = i.globals.colors.slice(); this.pushExtraColors(i.globals.colors);["fill", "stroke"].forEach((function (t) { void 0 === i.config[t].colors ? i.globals[t].colors = e.isColorFn ? i.config.colors : g : i.globals[t].colors = i.config[t].colors.slice(), e.pushExtraColors(i.globals[t].colors) })), void 0 === i.config.dataLabels.style.colors ? i.globals.dataLabels.style.colors = g : i.globals.dataLabels.style.colors = i.config.dataLabels.style.colors.slice(), this.pushExtraColors(i.globals.dataLabels.style.colors, 50), void 0 === i.config.plotOptions.radar.polygons.fill.colors ? i.globals.radarPolygons.fill.colors = ["dark" === i.config.theme.mode ? "#424242" : "none"] : i.globals.radarPolygons.fill.colors = i.config.plotOptions.radar.polygons.fill.colors.slice(), this.pushExtraColors(i.globals.radarPolygons.fill.colors, 20), void 0 === i.config.markers.colors ? i.globals.markers.colors = g : i.globals.markers.colors = i.config.markers.colors.slice(), this.pushExtraColors(i.globals.markers.colors) } }, { key: "pushExtraColors", value: function (t, e) { var i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : null, a = this.w, s = e || a.globals.series.length; if (null === i && (i = this.isBarDistributed || this.isHeatmapDistributed || "heatmap" === a.config.chart.type && a.config.plotOptions.heatmap.colorScale.inverse), i && a.globals.series.length && (s = a.globals.series[a.globals.maxValsInArrayIndex].length * a.globals.series.length), t.length < s) for (var r = s - t.length, o = 0; o < r; o++)t.push(t[o]) } }, { key: "updateThemeOptions", value: function (t) { t.chart = t.chart || {}, t.tooltip = t.tooltip || {}; var e = t.theme.mode || "light", i = t.theme.palette ? t.theme.palette : "dark" === e ? "palette4" : "palette1", a = t.chart.foreColor ? t.chart.foreColor : "dark" === e ? "#f6f7f8" : "#373d3f"; return t.tooltip.theme = e, t.chart.foreColor = a, t.theme.palette = i, t } }, { key: "predefined", value: function () { switch (this.w.config.theme.palette) { case "palette1": default: this.colors = ["#008FFB", "#00E396", "#FEB019", "#FF4560", "#775DD0"]; break; case "palette2": this.colors = ["#3f51b5", "#03a9f4", "#4caf50", "#f9ce1d", "#FF9800"]; break; case "palette3": this.colors = ["#33b2df", "#546E7A", "#d4526e", "#13d8aa", "#A5978B"]; break; case "palette4": this.colors = ["#4ecdc4", "#c7f464", "#81D4FA", "#fd6a6a", "#546E7A"]; break; case "palette5": this.colors = ["#2b908f", "#f9a3a4", "#90ee7e", "#fa4443", "#69d2e7"]; break; case "palette6": this.colors = ["#449DD1", "#F86624", "#EA3546", "#662E9B", "#C5D86D"]; break; case "palette7": this.colors = ["#D7263D", "#1B998B", "#2E294E", "#F46036", "#E2C044"]; break; case "palette8": this.colors = ["#662E9B", "#F86624", "#F9C80E", "#EA3546", "#43BCCD"]; break; case "palette9": this.colors = ["#5C4742", "#A5978B", "#8D5B4C", "#5A2A27", "#C4BBAF"]; break; case "palette10": this.colors = ["#A300D6", "#7D02EB", "#5653FE", "#2983FF", "#00B1F2"] }return this.colors } }]), t }(), et = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w } return r(t, [{ key: "draw", value: function () { this.drawTitleSubtitle("title"), this.drawTitleSubtitle("subtitle") } }, { key: "drawTitleSubtitle", value: function (t) { var e = this.w, i = "title" === t ? e.config.title : e.config.subtitle, a = e.globals.svgWidth / 2, s = i.offsetY, r = "middle"; if ("left" === i.align ? (a = 10, r = "start") : "right" === i.align && (a = e.globals.svgWidth - 10, r = "end"), a += i.offsetX, s = s + parseInt(i.style.fontSize, 10) + i.margin / 2, void 0 !== i.text) { var o = new m(this.ctx).drawText({ x: a, y: s, text: i.text, textAnchor: r, fontSize: i.style.fontSize, fontFamily: i.style.fontFamily, fontWeight: i.style.fontWeight, foreColor: i.style.color, opacity: 1 }); o.node.setAttribute("class", "apexcharts-".concat(t, "-text")), e.globals.dom.Paper.add(o) } } }]), t }(), it = function () { function t(e) { a(this, t), this.w = e.w, this.dCtx = e } return r(t, [{ key: "getTitleSubtitleCoords", value: function (t) { var e = this.w, i = 0, a = 0, s = "title" === t ? e.config.title.floating : e.config.subtitle.floating, r = e.globals.dom.baseEl.querySelector(".apexcharts-".concat(t, "-text")); if (null !== r && !s) { var o = r.getBoundingClientRect(); i = o.width, a = e.globals.axisCharts ? o.height + 5 : o.height } return { width: i, height: a } } }, { key: "getLegendsRect", value: function () { var t = this.w, e = t.globals.dom.elLegendWrap; t.config.legend.height || "top" !== t.config.legend.position && "bottom" !== t.config.legend.position || (e.style.maxHeight = t.globals.svgHeight / 2 + "px"); var i = Object.assign({}, x.getBoundingClientRect(e)); return null !== e && !t.config.legend.floating && t.config.legend.show ? this.dCtx.lgRect = { x: i.x, y: i.y, height: i.height, width: 0 === i.height ? 0 : i.width } : this.dCtx.lgRect = { x: 0, y: 0, height: 0, width: 0 }, "left" !== t.config.legend.position && "right" !== t.config.legend.position || 1.5 * this.dCtx.lgRect.width > t.globals.svgWidth && (this.dCtx.lgRect.width = t.globals.svgWidth / 1.5), this.dCtx.lgRect } }, { key: "getDatalabelsRect", value: function () { var t = this, e = this.w, i = []; e.config.series.forEach((function (s, r) { s.data.forEach((function (s, o) { var n; n = e.globals.series[r][o], a = e.config.dataLabels.formatter(n, { ctx: t.dCtx.ctx, seriesIndex: r, dataPointIndex: o, w: e }), i.push(a) })) })); var a = x.getLargestStringFromArr(i), s = new m(this.dCtx.ctx), r = e.config.dataLabels.style, o = s.getTextRects(a, parseInt(r.fontSize), r.fontFamily); return { width: 1.05 * o.width, height: o.height } } }, { key: "getLargestStringFromMultiArr", value: function (t, e) { var i = t; if (this.w.globals.isMultiLineX) { var a = e.map((function (t, e) { return Array.isArray(t) ? t.length : 1 })), s = Math.max.apply(Math, u(a)); i = e[a.indexOf(s)] } return i } }]), t }(), at = function () { function t(e) { a(this, t), this.w = e.w, this.dCtx = e } return r(t, [{ key: "getxAxisLabelsCoords", value: function () { var t, e = this.w, i = e.globals.labels.slice(); if (e.config.xaxis.convertedCatToNumeric && 0 === i.length && (i = e.globals.categoryLabels), e.globals.timescaleLabels.length > 0) { var a = this.getxAxisTimeScaleLabelsCoords(); t = { width: a.width, height: a.height }, e.globals.rotateXLabels = !1 } else { this.dCtx.lgWidthForSideLegends = "left" !== e.config.legend.position && "right" !== e.config.legend.position || e.config.legend.floating ? 0 : this.dCtx.lgRect.width; var s = e.globals.xLabelFormatter, r = x.getLargestStringFromArr(i), o = this.dCtx.dimHelpers.getLargestStringFromMultiArr(r, i); e.globals.isBarHorizontal && (o = r = e.globals.yAxisScale[0].result.reduce((function (t, e) { return t.length > e.length ? t : e }), 0)); var n = new S(this.dCtx.ctx), l = r; r = n.xLabelFormat(s, r, l, { i: void 0, dateFormatter: new A(this.dCtx.ctx).formatDate, w: e }), o = n.xLabelFormat(s, o, l, { i: void 0, dateFormatter: new A(this.dCtx.ctx).formatDate, w: e }), (e.config.xaxis.convertedCatToNumeric && void 0 === r || "" === String(r).trim()) && (o = r = "1"); var h = new m(this.dCtx.ctx), c = h.getTextRects(r, e.config.xaxis.labels.style.fontSize), d = c; if (r !== o && (d = h.getTextRects(o, e.config.xaxis.labels.style.fontSize)), (t = { width: c.width >= d.width ? c.width : d.width, height: c.height >= d.height ? c.height : d.height }).width * i.length > e.globals.svgWidth - this.dCtx.lgWidthForSideLegends - this.dCtx.yAxisWidth - this.dCtx.gridPad.left - this.dCtx.gridPad.right && 0 !== e.config.xaxis.labels.rotate || e.config.xaxis.labels.rotateAlways) { if (!e.globals.isBarHorizontal) { e.globals.rotateXLabels = !0; var g = function (t) { return h.getTextRects(t, e.config.xaxis.labels.style.fontSize, e.config.xaxis.labels.style.fontFamily, "rotate(".concat(e.config.xaxis.labels.rotate, " 0 0)"), !1) }; c = g(r), r !== o && (d = g(o)), t.height = (c.height > d.height ? c.height : d.height) / 1.5, t.width = c.width > d.width ? c.width : d.width } } else e.globals.rotateXLabels = !1 } return e.config.xaxis.labels.show || (t = { width: 0, height: 0 }), { width: t.width, height: t.height } } }, { key: "getxAxisGroupLabelsCoords", value: function () { var t, e = this.w; if (!e.globals.hasXaxisGroups) return { width: 0, height: 0 }; var i, a = (null === (t = e.config.xaxis.group.style) || void 0 === t ? void 0 : t.fontSize) || e.config.xaxis.labels.style.fontSize, s = e.globals.groups.map((function (t) { return t.title })), r = x.getLargestStringFromArr(s), o = this.dCtx.dimHelpers.getLargestStringFromMultiArr(r, s), n = new m(this.dCtx.ctx), l = n.getTextRects(r, a), h = l; return r !== o && (h = n.getTextRects(o, a)), i = { width: l.width >= h.width ? l.width : h.width, height: l.height >= h.height ? l.height : h.height }, e.config.xaxis.labels.show || (i = { width: 0, height: 0 }), { width: i.width, height: i.height } } }, { key: "getxAxisTitleCoords", value: function () { var t = this.w, e = 0, i = 0; if (void 0 !== t.config.xaxis.title.text) { var a = new m(this.dCtx.ctx).getTextRects(t.config.xaxis.title.text, t.config.xaxis.title.style.fontSize); e = a.width, i = a.height } return { width: e, height: i } } }, { key: "getxAxisTimeScaleLabelsCoords", value: function () { var t, e = this.w; this.dCtx.timescaleLabels = e.globals.timescaleLabels.slice(); var i = this.dCtx.timescaleLabels.map((function (t) { return t.value })), a = i.reduce((function (t, e) { return void 0 === t ? (console.error("You have possibly supplied invalid Date format. Please supply a valid JavaScript Date"), 0) : t.length > e.length ? t : e }), 0); return 1.05 * (t = new m(this.dCtx.ctx).getTextRects(a, e.config.xaxis.labels.style.fontSize)).width * i.length > e.globals.gridWidth && 0 !== e.config.xaxis.labels.rotate && (e.globals.overlappingXLabels = !0), t } }, { key: "additionalPaddingXLabels", value: function (t) { var e = this, i = this.w, a = i.globals, s = i.config, r = s.xaxis.type, o = t.width; a.skipLastTimelinelabel = !1, a.skipFirstTimelinelabel = !1; var n = i.config.yaxis[0].opposite && i.globals.isBarHorizontal, l = function (t, n) { s.yaxis.length > 1 && function (t) { return -1 !== a.collapsedSeriesIndices.indexOf(t) }(n) || function (t) { if (e.dCtx.timescaleLabels && e.dCtx.timescaleLabels.length) { var n = e.dCtx.timescaleLabels[0], l = e.dCtx.timescaleLabels[e.dCtx.timescaleLabels.length - 1].position + o / 1.75 - e.dCtx.yAxisWidthRight, h = n.position - o / 1.75 + e.dCtx.yAxisWidthLeft, c = "right" === i.config.legend.position && e.dCtx.lgRect.width > 0 ? e.dCtx.lgRect.width : 0; l > a.svgWidth - a.translateX - c && (a.skipLastTimelinelabel = !0), h < -(t.show && !t.floating || "bar" !== s.chart.type && "candlestick" !== s.chart.type && "rangeBar" !== s.chart.type && "boxPlot" !== s.chart.type ? 10 : o / 1.75) && (a.skipFirstTimelinelabel = !0) } else "datetime" === r ? e.dCtx.gridPad.right < o && !a.rotateXLabels && (a.skipLastTimelinelabel = !0) : "datetime" !== r && e.dCtx.gridPad.right < o / 2 - e.dCtx.yAxisWidthRight && !a.rotateXLabels && !i.config.xaxis.labels.trim && ("between" !== i.config.xaxis.tickPlacement || i.globals.isBarHorizontal) && (e.dCtx.xPadRight = o / 2 + 1) }(t) }; s.yaxis.forEach((function (t, i) { n ? (e.dCtx.gridPad.left < o && (e.dCtx.xPadLeft = o / 2 + 1), e.dCtx.xPadRight = o / 2 + 1) : l(t, i) })) } }]), t }(), st = function () { function t(e) { a(this, t), this.w = e.w, this.dCtx = e } return r(t, [{ key: "getyAxisLabelsCoords", value: function () { var t = this, e = this.w, i = [], a = 10, s = new C(this.dCtx.ctx); return e.config.yaxis.map((function (r, o) { var n = { seriesIndex: o, dataPointIndex: -1, w: e }, l = e.globals.yAxisScale[o], h = 0; if (!s.isYAxisHidden(o) && r.labels.show && void 0 !== r.labels.minWidth && (h = r.labels.minWidth), !s.isYAxisHidden(o) && r.labels.show && l.result.length) { var c = e.globals.yLabelFormatters[o], d = l.niceMin === Number.MIN_VALUE ? 0 : l.niceMin, g = l.result.reduce((function (t, e) { var i, a; return (null === (i = String(c(t, n))) || void 0 === i ? void 0 : i.length) > (null === (a = String(c(e, n))) || void 0 === a ? void 0 : a.length) ? t : e }), d), u = g = c(g, n); if (void 0 !== g && 0 !== g.length || (g = l.niceMax), e.globals.isBarHorizontal) { a = 0; var p = e.globals.labels.slice(); g = x.getLargestStringFromArr(p), g = c(g, { seriesIndex: o, dataPointIndex: -1, w: e }), u = t.dCtx.dimHelpers.getLargestStringFromMultiArr(g, p) } var f = new m(t.dCtx.ctx), b = "rotate(".concat(r.labels.rotate, " 0 0)"), v = f.getTextRects(g, r.labels.style.fontSize, r.labels.style.fontFamily, b, !1), y = v; g !== u && (y = f.getTextRects(u, r.labels.style.fontSize, r.labels.style.fontFamily, b, !1)), i.push({ width: (h > y.width || h > v.width ? h : y.width > v.width ? y.width : v.width) + a, height: y.height > v.height ? y.height : v.height }) } else i.push({ width: 0, height: 0 }) })), i } }, { key: "getyAxisTitleCoords", value: function () { var t = this, e = this.w, i = []; return e.config.yaxis.map((function (e, a) { if (e.show && void 0 !== e.title.text) { var s = new m(t.dCtx.ctx), r = "rotate(".concat(e.title.rotate, " 0 0)"), o = s.getTextRects(e.title.text, e.title.style.fontSize, e.title.style.fontFamily, r, !1); i.push({ width: o.width, height: o.height }) } else i.push({ width: 0, height: 0 }) })), i } }, { key: "getTotalYAxisWidth", value: function () { var t = this.w, e = 0, i = 0, a = 0, s = t.globals.yAxisScale.length > 1 ? 10 : 0, r = new C(this.dCtx.ctx), o = function (o, n) { var l = t.config.yaxis[n].floating, h = 0; o.width > 0 && !l ? (h = o.width + s, function (e) { return t.globals.ignoreYAxisIndexes.indexOf(e) > -1 }(n) && (h = h - o.width - s)) : h = l || r.isYAxisHidden(n) ? 0 : 5, t.config.yaxis[n].opposite ? a += h : i += h, e += h }; return t.globals.yLabelsCoords.map((function (t, e) { o(t, e) })), t.globals.yTitleCoords.map((function (t, e) { o(t, e) })), t.globals.isBarHorizontal && !t.config.yaxis[0].floating && (e = t.globals.yLabelsCoords[0].width + t.globals.yTitleCoords[0].width + 15), this.dCtx.yAxisWidthLeft = i, this.dCtx.yAxisWidthRight = a, e } }]), t }(), rt = function () { function t(e) { a(this, t), this.w = e.w, this.dCtx = e } return r(t, [{ key: "gridPadForColumnsInNumericAxis", value: function (t) { var e = this.w, i = e.config, a = e.globals; if (a.noData || a.collapsedSeries.length + a.ancillaryCollapsedSeries.length === i.series.length) return 0; var s = function (t) { return "bar" === t || "rangeBar" === t || "candlestick" === t || "boxPlot" === t }, r = i.chart.type, o = 0, n = s(r) ? i.series.length : 1; if (a.comboBarCount > 0 && (n = a.comboBarCount), a.collapsedSeries.forEach((function (t) { s(t.type) && (n -= 1) })), i.chart.stacked && (n = 1), (s(r) || a.comboBarCount > 0) && a.isXNumeric && !a.isBarHorizontal && n > 0) { var l, h, c = Math.abs(a.initialMaxX - a.initialMinX); c <= 3 && (c = a.dataPoints), l = c / t, a.minXDiff && a.minXDiff / l > 0 && (h = a.minXDiff / l), h > t / 2 && (h /= 2), (o = h * parseInt(i.plotOptions.bar.columnWidth, 10) / 100) < 1 && (o = 1), a.barPadForNumericAxis = o } return o } }, { key: "gridPadFortitleSubtitle", value: function () { var t = this, e = this.w, i = e.globals, a = this.dCtx.isSparkline || !e.globals.axisCharts ? 0 : 10;["title", "subtitle"].forEach((function (i) { void 0 !== e.config[i].text ? a += e.config[i].margin : a += t.dCtx.isSparkline || !e.globals.axisCharts ? 0 : 5 })), !e.config.legend.show || "bottom" !== e.config.legend.position || e.config.legend.floating || e.globals.axisCharts || (a += 10); var s = this.dCtx.dimHelpers.getTitleSubtitleCoords("title"), r = this.dCtx.dimHelpers.getTitleSubtitleCoords("subtitle"); i.gridHeight = i.gridHeight - s.height - r.height - a, i.translateY = i.translateY + s.height + r.height + a } }, { key: "setGridXPosForDualYAxis", value: function (t, e) { var i = this.w, a = new C(this.dCtx.ctx); i.config.yaxis.map((function (s, r) { -1 !== i.globals.ignoreYAxisIndexes.indexOf(r) || s.floating || a.isYAxisHidden(r) || (s.opposite && (i.globals.translateX = i.globals.translateX - (e[r].width + t[r].width) - parseInt(i.config.yaxis[r].labels.style.fontSize, 10) / 1.2 - 12), i.globals.translateX < 2 && (i.globals.translateX = 2)) })) } }]), t }(), ot = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w, this.lgRect = {}, this.yAxisWidth = 0, this.yAxisWidthLeft = 0, this.yAxisWidthRight = 0, this.xAxisHeight = 0, this.isSparkline = this.w.config.chart.sparkline.enabled, this.dimHelpers = new it(this), this.dimYAxis = new st(this), this.dimXAxis = new at(this), this.dimGrid = new rt(this), this.lgWidthForSideLegends = 0, this.gridPad = this.w.config.grid.padding, this.xPadRight = 0, this.xPadLeft = 0 } return r(t, [{ key: "plotCoords", value: function () { var t = this, e = this.w, i = e.globals; this.lgRect = this.dimHelpers.getLegendsRect(), this.datalabelsCoords = { width: 0, height: 0 }; var a = Array.isArray(e.config.stroke.width) ? Math.max.apply(Math, u(e.config.stroke.width)) : e.config.stroke.width; this.isSparkline && ((e.config.markers.discrete.length > 0 || e.config.markers.size > 0) && Object.entries(this.gridPad).forEach((function (e) { var i = g(e, 2), a = i[0], s = i[1]; t.gridPad[a] = Math.max(s, t.w.globals.markers.largestSize / 1.5) })), this.gridPad.top = Math.max(a / 2, this.gridPad.top), this.gridPad.bottom = Math.max(a / 2, this.gridPad.bottom)), i.axisCharts ? this.setDimensionsForAxisCharts() : this.setDimensionsForNonAxisCharts(), this.dimGrid.gridPadFortitleSubtitle(), i.gridHeight = i.gridHeight - this.gridPad.top - this.gridPad.bottom, i.gridWidth = i.gridWidth - this.gridPad.left - this.gridPad.right - this.xPadRight - this.xPadLeft; var s = this.dimGrid.gridPadForColumnsInNumericAxis(i.gridWidth); i.gridWidth = i.gridWidth - 2 * s, i.translateX = i.translateX + this.gridPad.left + this.xPadLeft + (s > 0 ? s : 0), i.translateY = i.translateY + this.gridPad.top } }, { key: "setDimensionsForAxisCharts", value: function () { var t = this, e = this.w, i = e.globals, a = this.dimYAxis.getyAxisLabelsCoords(), s = this.dimYAxis.getyAxisTitleCoords(); i.isSlopeChart && (this.datalabelsCoords = this.dimHelpers.getDatalabelsRect()), e.globals.yLabelsCoords = [], e.globals.yTitleCoords = [], e.config.yaxis.map((function (t, i) { e.globals.yLabelsCoords.push({ width: a[i].width, index: i }), e.globals.yTitleCoords.push({ width: s[i].width, index: i }) })), this.yAxisWidth = this.dimYAxis.getTotalYAxisWidth(); var r = this.dimXAxis.getxAxisLabelsCoords(), o = this.dimXAxis.getxAxisGroupLabelsCoords(), n = this.dimXAxis.getxAxisTitleCoords(); this.conditionalChecksForAxisCoords(r, n, o), i.translateXAxisY = e.globals.rotateXLabels ? this.xAxisHeight / 8 : -4, i.translateXAxisX = e.globals.rotateXLabels && e.globals.isXNumeric && e.config.xaxis.labels.rotate <= -45 ? -this.xAxisWidth / 4 : 0, e.globals.isBarHorizontal && (i.rotateXLabels = !1, i.translateXAxisY = parseInt(e.config.xaxis.labels.style.fontSize, 10) / 1.5 * -1), i.translateXAxisY = i.translateXAxisY + e.config.xaxis.labels.offsetY, i.translateXAxisX = i.translateXAxisX + e.config.xaxis.labels.offsetX; var l = this.yAxisWidth, h = this.xAxisHeight; i.xAxisLabelsHeight = this.xAxisHeight - n.height, i.xAxisGroupLabelsHeight = i.xAxisLabelsHeight - r.height, i.xAxisLabelsWidth = this.xAxisWidth, i.xAxisHeight = this.xAxisHeight; var c = 10; ("radar" === e.config.chart.type || this.isSparkline) && (l = 0, h = i.goldenPadding), this.isSparkline && (this.lgRect = { height: 0, width: 0 }), (this.isSparkline || "treemap" === e.config.chart.type) && (l = 0, h = 0, c = 0), this.isSparkline || this.dimXAxis.additionalPaddingXLabels(r); var d = function () { i.translateX = l + t.datalabelsCoords.width, i.gridHeight = i.svgHeight - t.lgRect.height - h - (t.isSparkline || "treemap" === e.config.chart.type ? 0 : e.globals.rotateXLabels ? 10 : 15), i.gridWidth = i.svgWidth - l - 2 * t.datalabelsCoords.width }; switch ("top" === e.config.xaxis.position && (c = i.xAxisHeight - e.config.xaxis.axisTicks.height - 5), e.config.legend.position) { case "bottom": i.translateY = c, d(); break; case "top": i.translateY = this.lgRect.height + c, d(); break; case "left": i.translateY = c, i.translateX = this.lgRect.width + l + this.datalabelsCoords.width, i.gridHeight = i.svgHeight - h - 12, i.gridWidth = i.svgWidth - this.lgRect.width - l - 2 * this.datalabelsCoords.width; break; case "right": i.translateY = c, i.translateX = l + this.datalabelsCoords.width, i.gridHeight = i.svgHeight - h - 12, i.gridWidth = i.svgWidth - this.lgRect.width - l - 2 * this.datalabelsCoords.width - 5; break; default: throw new Error("Legend position not supported") }this.dimGrid.setGridXPosForDualYAxis(s, a), new q(this.ctx).setYAxisXPosition(a, s) } }, { key: "setDimensionsForNonAxisCharts", value: function () { var t = this.w, e = t.globals, i = t.config, a = 0; t.config.legend.show && !t.config.legend.floating && (a = 20); var s = "pie" === i.chart.type || "polarArea" === i.chart.type || "donut" === i.chart.type ? "pie" : "radialBar", r = i.plotOptions[s].offsetY, o = i.plotOptions[s].offsetX; if (!i.legend.show || i.legend.floating) return e.gridHeight = e.svgHeight - i.grid.padding.left + i.grid.padding.right, e.gridWidth = Math.min(e.svgWidth, e.gridHeight), e.translateY = r, void (e.translateX = o + (e.svgWidth - e.gridWidth) / 2); switch (i.legend.position) { case "bottom": e.gridHeight = e.svgHeight - this.lgRect.height - e.goldenPadding, e.gridWidth = e.svgWidth, e.translateY = r - 10, e.translateX = o + (e.svgWidth - e.gridWidth) / 2; break; case "top": e.gridHeight = e.svgHeight - this.lgRect.height - e.goldenPadding, e.gridWidth = e.svgWidth, e.translateY = this.lgRect.height + r + 10, e.translateX = o + (e.svgWidth - e.gridWidth) / 2; break; case "left": e.gridWidth = e.svgWidth - this.lgRect.width - a, e.gridHeight = "auto" !== i.chart.height ? e.svgHeight : e.gridWidth, e.translateY = r, e.translateX = o + this.lgRect.width + a; break; case "right": e.gridWidth = e.svgWidth - this.lgRect.width - a - 5, e.gridHeight = "auto" !== i.chart.height ? e.svgHeight : e.gridWidth, e.translateY = r, e.translateX = o + 10; break; default: throw new Error("Legend position not supported") } } }, { key: "conditionalChecksForAxisCoords", value: function (t, e, i) { var a = this.w, s = a.globals.hasXaxisGroups ? 2 : 1, r = i.height + t.height + e.height, o = a.globals.isMultiLineX ? 1.2 : a.globals.LINE_HEIGHT_RATIO, n = a.globals.rotateXLabels ? 22 : 10, l = a.globals.rotateXLabels && "bottom" === a.config.legend.position ? 10 : 0; this.xAxisHeight = r * o + s * n + l, this.xAxisWidth = t.width, this.xAxisHeight - e.height > a.config.xaxis.labels.maxHeight && (this.xAxisHeight = a.config.xaxis.labels.maxHeight), a.config.xaxis.labels.minHeight && this.xAxisHeight < a.config.xaxis.labels.minHeight && (this.xAxisHeight = a.config.xaxis.labels.minHeight), a.config.xaxis.floating && (this.xAxisHeight = 0); var h = 0, c = 0; a.config.yaxis.forEach((function (t) { h += t.labels.minWidth, c += t.labels.maxWidth })), this.yAxisWidth < h && (this.yAxisWidth = h), this.yAxisWidth > c && (this.yAxisWidth = c) } }]), t }(), nt = function () { function t(e) { a(this, t), this.w = e.w, this.lgCtx = e } return r(t, [{ key: "getLegendStyles", value: function () { var t, e, i, a = document.createElement("style"); a.setAttribute("type", "text/css"); var s = (null === (t = this.lgCtx.ctx) || void 0 === t || null === (e = t.opts) || void 0 === e || null === (i = e.chart) || void 0 === i ? void 0 : i.nonce) || this.w.config.chart.nonce; s && a.setAttribute("nonce", s); var r = document.createTextNode("\n .apexcharts-legend {\n display: flex;\n overflow: auto;\n padding: 0 10px;\n }\n .apexcharts-legend.apx-legend-position-bottom, .apexcharts-legend.apx-legend-position-top {\n flex-wrap: wrap\n }\n .apexcharts-legend.apx-legend-position-right, .apexcharts-legend.apx-legend-position-left {\n flex-direction: column;\n bottom: 0;\n }\n .apexcharts-legend.apx-legend-position-bottom.apexcharts-align-left, .apexcharts-legend.apx-legend-position-top.apexcharts-align-left, .apexcharts-legend.apx-legend-position-right, .apexcharts-legend.apx-legend-position-left {\n justify-content: flex-start;\n }\n .apexcharts-legend.apx-legend-position-bottom.apexcharts-align-center, .apexcharts-legend.apx-legend-position-top.apexcharts-align-center {\n justify-content: center;\n }\n .apexcharts-legend.apx-legend-position-bottom.apexcharts-align-right, .apexcharts-legend.apx-legend-position-top.apexcharts-align-right {\n justify-content: flex-end;\n }\n .apexcharts-legend-series {\n cursor: pointer;\n line-height: normal;\n }\n .apexcharts-legend.apx-legend-position-bottom .apexcharts-legend-series, .apexcharts-legend.apx-legend-position-top .apexcharts-legend-series{\n display: flex;\n align-items: center;\n }\n .apexcharts-legend-text {\n position: relative;\n font-size: 14px;\n }\n .apexcharts-legend-text *, .apexcharts-legend-marker * {\n pointer-events: none;\n }\n .apexcharts-legend-marker {\n position: relative;\n display: inline-block;\n cursor: pointer;\n margin-right: 3px;\n border-style: solid;\n }\n\n .apexcharts-legend.apexcharts-align-right .apexcharts-legend-series, .apexcharts-legend.apexcharts-align-left .apexcharts-legend-series{\n display: inline-block;\n }\n .apexcharts-legend-series.apexcharts-no-click {\n cursor: auto;\n }\n .apexcharts-legend .apexcharts-hidden-zero-series, .apexcharts-legend .apexcharts-hidden-null-series {\n display: none !important;\n }\n .apexcharts-inactive-legend {\n opacity: 0.45;\n }"); return a.appendChild(r), a } }, { key: "getLegendBBox", value: function () { var t = this.w.globals.dom.baseEl.querySelector(".apexcharts-legend").getBoundingClientRect(), e = t.width; return { clwh: t.height, clww: e } } }, { key: "appendToForeignObject", value: function () { this.w.globals.dom.elLegendForeign.appendChild(this.getLegendStyles()) } }, { key: "toggleDataSeries", value: function (t, e) { var i = this, a = this.w; if (a.globals.axisCharts || "radialBar" === a.config.chart.type) { a.globals.resized = !0; var s = null, r = null; if (a.globals.risingSeries = [], a.globals.axisCharts ? (s = a.globals.dom.baseEl.querySelector(".apexcharts-series[data\\:realIndex='".concat(t, "']")), r = parseInt(s.getAttribute("data:realIndex"), 10)) : (s = a.globals.dom.baseEl.querySelector(".apexcharts-series[rel='".concat(t + 1, "']")), r = parseInt(s.getAttribute("rel"), 10) - 1), e) [{ cs: a.globals.collapsedSeries, csi: a.globals.collapsedSeriesIndices }, { cs: a.globals.ancillaryCollapsedSeries, csi: a.globals.ancillaryCollapsedSeriesIndices }].forEach((function (t) { i.riseCollapsedSeries(t.cs, t.csi, r) })); else this.hideSeries({ seriesEl: s, realIndex: r }) } else { var o = a.globals.dom.Paper.select(" .apexcharts-series[rel='".concat(t + 1, "'] path")), n = a.config.chart.type; if ("pie" === n || "polarArea" === n || "donut" === n) { var l = a.config.plotOptions.pie.donut.labels; new m(this.lgCtx.ctx).pathMouseDown(o.members[0], null), this.lgCtx.ctx.pie.printDataLabelsInner(o.members[0].node, l) } o.fire("click") } } }, { key: "hideSeries", value: function (t) { var e = t.seriesEl, i = t.realIndex, a = this.w, s = a.globals, r = x.clone(a.config.series); if (s.axisCharts) { var o = a.config.yaxis[s.seriesYAxisReverseMap[i]]; if (o && o.show && o.showAlways) s.ancillaryCollapsedSeriesIndices.indexOf(i) < 0 && (s.ancillaryCollapsedSeries.push({ index: i, data: r[i].data.slice(), type: e.parentNode.className.baseVal.split("-")[1] }), s.ancillaryCollapsedSeriesIndices.push(i)); else if (s.collapsedSeriesIndices.indexOf(i) < 0) { s.collapsedSeries.push({ index: i, data: r[i].data.slice(), type: e.parentNode.className.baseVal.split("-")[1] }), s.collapsedSeriesIndices.push(i); var n = s.risingSeries.indexOf(i); s.risingSeries.splice(n, 1) } } else s.collapsedSeries.push({ index: i, data: r[i] }), s.collapsedSeriesIndices.push(i); for (var l = e.childNodes, h = 0; h < l.length; h++)l[h].classList.contains("apexcharts-series-markers-wrap") && (l[h].classList.contains("apexcharts-hide") ? l[h].classList.remove("apexcharts-hide") : l[h].classList.add("apexcharts-hide")); s.allSeriesCollapsed = s.collapsedSeries.length + s.ancillaryCollapsedSeries.length === a.config.series.length, r = this._getSeriesBasedOnCollapsedState(r), this.lgCtx.ctx.updateHelpers._updateSeries(r, a.config.chart.animations.dynamicAnimation.enabled) } }, { key: "riseCollapsedSeries", value: function (t, e, i) { var a = this.w, s = x.clone(a.config.series); if (t.length > 0) { for (var r = 0; r < t.length; r++)t[r].index === i && (a.globals.axisCharts ? (s[i].data = t[r].data.slice(), t.splice(r, 1), e.splice(r, 1), a.globals.risingSeries.push(i)) : (s[i] = t[r].data, t.splice(r, 1), e.splice(r, 1), a.globals.risingSeries.push(i))); s = this._getSeriesBasedOnCollapsedState(s), this.lgCtx.ctx.updateHelpers._updateSeries(s, a.config.chart.animations.dynamicAnimation.enabled) } } }, { key: "_getSeriesBasedOnCollapsedState", value: function (t) { var e = this.w, i = 0; return e.globals.axisCharts ? t.forEach((function (a, s) { e.globals.collapsedSeriesIndices.indexOf(s) < 0 && e.globals.ancillaryCollapsedSeriesIndices.indexOf(s) < 0 || (t[s].data = [], i++) })) : t.forEach((function (a, s) { !e.globals.collapsedSeriesIndices.indexOf(s) < 0 && (t[s] = 0, i++) })), e.globals.allSeriesCollapsed = i === t.length, t } }]), t }(), lt = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w, this.onLegendClick = this.onLegendClick.bind(this), this.onLegendHovered = this.onLegendHovered.bind(this), this.isBarsDistributed = "bar" === this.w.config.chart.type && this.w.config.plotOptions.bar.distributed && 1 === this.w.config.series.length, this.legendHelpers = new nt(this) } return r(t, [{ key: "init", value: function () { var t = this.w, e = t.globals, i = t.config; if ((i.legend.showForSingleSeries && 1 === e.series.length || this.isBarsDistributed || e.series.length > 1 || !e.axisCharts) && i.legend.show) { for (; e.dom.elLegendWrap.firstChild;)e.dom.elLegendWrap.removeChild(e.dom.elLegendWrap.firstChild); this.drawLegends(), x.isIE11() ? document.getElementsByTagName("head")[0].appendChild(this.legendHelpers.getLegendStyles()) : this.legendHelpers.appendToForeignObject(), "bottom" === i.legend.position || "top" === i.legend.position ? this.legendAlignHorizontal() : "right" !== i.legend.position && "left" !== i.legend.position || this.legendAlignVertical() } } }, { key: "drawLegends", value: function () { var t = this, e = this.w, i = e.config.legend.fontFamily, a = e.globals.seriesNames, s = e.globals.colors.slice(); if ("heatmap" === e.config.chart.type) { var r = e.config.plotOptions.heatmap.colorScale.ranges; a = r.map((function (t) { return t.name ? t.name : t.from + " - " + t.to })), s = r.map((function (t) { return t.color })) } else this.isBarsDistributed && (a = e.globals.labels.slice()); e.config.legend.customLegendItems.length && (a = e.config.legend.customLegendItems); for (var o = e.globals.legendFormatter, n = e.config.legend.inverseOrder, l = n ? a.length - 1 : 0; n ? l >= 0 : l <= a.length - 1; n ? l-- : l++) { var h, c = o(a[l], { seriesIndex: l, w: e }), d = !1, g = !1; if (e.globals.collapsedSeries.length > 0) for (var u = 0; u < e.globals.collapsedSeries.length; u++)e.globals.collapsedSeries[u].index === l && (d = !0); if (e.globals.ancillaryCollapsedSeriesIndices.length > 0) for (var p = 0; p < e.globals.ancillaryCollapsedSeriesIndices.length; p++)e.globals.ancillaryCollapsedSeriesIndices[p] === l && (g = !0); var f = document.createElement("span"); f.classList.add("apexcharts-legend-marker"); var b = e.config.legend.markers.offsetX, v = e.config.legend.markers.offsetY, w = e.config.legend.markers.height, k = e.config.legend.markers.width, A = e.config.legend.markers.strokeWidth, S = e.config.legend.markers.strokeColor, C = e.config.legend.markers.radius, L = f.style; L.background = s[l], L.color = s[l], L.setProperty("background", s[l], "important"), e.config.legend.markers.fillColors && e.config.legend.markers.fillColors[l] && (L.background = e.config.legend.markers.fillColors[l]), void 0 !== e.globals.seriesColors[l] && (L.background = e.globals.seriesColors[l], L.color = e.globals.seriesColors[l]), L.height = Array.isArray(w) ? parseFloat(w[l]) + "px" : parseFloat(w) + "px", L.width = Array.isArray(k) ? parseFloat(k[l]) + "px" : parseFloat(k) + "px", L.left = (Array.isArray(b) ? parseFloat(b[l]) : parseFloat(b)) + "px", L.top = (Array.isArray(v) ? parseFloat(v[l]) : parseFloat(v)) + "px", L.borderWidth = Array.isArray(A) ? A[l] : A, L.borderColor = Array.isArray(S) ? S[l] : S, L.borderRadius = Array.isArray(C) ? parseFloat(C[l]) + "px" : parseFloat(C) + "px", e.config.legend.markers.customHTML && (Array.isArray(e.config.legend.markers.customHTML) ? e.config.legend.markers.customHTML[l] && (f.innerHTML = e.config.legend.markers.customHTML[l]()) : f.innerHTML = e.config.legend.markers.customHTML()), m.setAttrs(f, { rel: l + 1, "data:collapsed": d || g }), (d || g) && f.classList.add("apexcharts-inactive-legend"); var P = document.createElement("div"), M = document.createElement("span"); M.classList.add("apexcharts-legend-text"), M.innerHTML = Array.isArray(c) ? c.join(" ") : c; var I = e.config.legend.labels.useSeriesColors ? e.globals.colors[l] : Array.isArray(e.config.legend.labels.colors) ? null === (h = e.config.legend.labels.colors) || void 0 === h ? void 0 : h[l] : e.config.legend.labels.colors; I || (I = e.config.chart.foreColor), M.style.color = I, M.style.fontSize = parseFloat(e.config.legend.fontSize) + "px", M.style.fontWeight = e.config.legend.fontWeight, M.style.fontFamily = i || e.config.chart.fontFamily, m.setAttrs(M, { rel: l + 1, i: l, "data:default-text": encodeURIComponent(c), "data:collapsed": d || g }), P.appendChild(f), P.appendChild(M); var T = new y(this.ctx); if (!e.config.legend.showForZeroSeries) 0 === T.getSeriesTotalByIndex(l) && T.seriesHaveSameValues(l) && !T.isSeriesNull(l) && -1 === e.globals.collapsedSeriesIndices.indexOf(l) && -1 === e.globals.ancillaryCollapsedSeriesIndices.indexOf(l) && P.classList.add("apexcharts-hidden-zero-series"); e.config.legend.showForNullSeries || T.isSeriesNull(l) && -1 === e.globals.collapsedSeriesIndices.indexOf(l) && -1 === e.globals.ancillaryCollapsedSeriesIndices.indexOf(l) && P.classList.add("apexcharts-hidden-null-series"), e.globals.dom.elLegendWrap.appendChild(P), e.globals.dom.elLegendWrap.classList.add("apexcharts-align-".concat(e.config.legend.horizontalAlign)), e.globals.dom.elLegendWrap.classList.add("apx-legend-position-" + e.config.legend.position), P.classList.add("apexcharts-legend-series"), P.style.margin = "".concat(e.config.legend.itemMargin.vertical, "px ").concat(e.config.legend.itemMargin.horizontal, "px"), e.globals.dom.elLegendWrap.style.width = e.config.legend.width ? e.config.legend.width + "px" : "", e.globals.dom.elLegendWrap.style.height = e.config.legend.height ? e.config.legend.height + "px" : "", m.setAttrs(P, { rel: l + 1, seriesName: x.escapeString(a[l]), "data:collapsed": d || g }), (d || g) && P.classList.add("apexcharts-inactive-legend"), e.config.legend.onItemClick.toggleDataSeries || P.classList.add("apexcharts-no-click") } e.globals.dom.elWrap.addEventListener("click", t.onLegendClick, !0), e.config.legend.onItemHover.highlightDataSeries && 0 === e.config.legend.customLegendItems.length && (e.globals.dom.elWrap.addEventListener("mousemove", t.onLegendHovered, !0), e.globals.dom.elWrap.addEventListener("mouseout", t.onLegendHovered, !0)) } }, { key: "setLegendWrapXY", value: function (t, e) { var i = this.w, a = i.globals.dom.elLegendWrap, s = a.getBoundingClientRect(), r = 0, o = 0; if ("bottom" === i.config.legend.position) o += i.globals.svgHeight - s.height / 2; else if ("top" === i.config.legend.position) { var n = new ot(this.ctx), l = n.dimHelpers.getTitleSubtitleCoords("title").height, h = n.dimHelpers.getTitleSubtitleCoords("subtitle").height; o = o + (l > 0 ? l - 10 : 0) + (h > 0 ? h - 10 : 0) } a.style.position = "absolute", r = r + t + i.config.legend.offsetX, o = o + e + i.config.legend.offsetY, a.style.left = r + "px", a.style.top = o + "px", "bottom" === i.config.legend.position ? (a.style.top = "auto", a.style.bottom = 5 - i.config.legend.offsetY + "px") : "right" === i.config.legend.position && (a.style.left = "auto", a.style.right = 25 + i.config.legend.offsetX + "px");["width", "height"].forEach((function (t) { a.style[t] && (a.style[t] = parseInt(i.config.legend[t], 10) + "px") })) } }, { key: "legendAlignHorizontal", value: function () { var t = this.w; t.globals.dom.elLegendWrap.style.right = 0; var e = this.legendHelpers.getLegendBBox(), i = new ot(this.ctx), a = i.dimHelpers.getTitleSubtitleCoords("title"), s = i.dimHelpers.getTitleSubtitleCoords("subtitle"), r = 0; "bottom" === t.config.legend.position ? r = -e.clwh / 1.8 : "top" === t.config.legend.position && (r = a.height + s.height + t.config.title.margin + t.config.subtitle.margin - 10), this.setLegendWrapXY(20, r) } }, { key: "legendAlignVertical", value: function () { var t = this.w, e = this.legendHelpers.getLegendBBox(), i = 0; "left" === t.config.legend.position && (i = 20), "right" === t.config.legend.position && (i = t.globals.svgWidth - e.clww - 10), this.setLegendWrapXY(i, 20) } }, { key: "onLegendHovered", value: function (t) { var e = this.w, i = t.target.classList.contains("apexcharts-legend-series") || t.target.classList.contains("apexcharts-legend-text") || t.target.classList.contains("apexcharts-legend-marker"); if ("heatmap" === e.config.chart.type || this.isBarsDistributed) { if (i) { var a = parseInt(t.target.getAttribute("rel"), 10) - 1; this.ctx.events.fireEvent("legendHover", [this.ctx, a, this.w]), new W(this.ctx).highlightRangeInSeries(t, t.target) } } else !t.target.classList.contains("apexcharts-inactive-legend") && i && new W(this.ctx).toggleSeriesOnHover(t, t.target) } }, { key: "onLegendClick", value: function (t) { var e = this.w; if (!e.config.legend.customLegendItems.length && (t.target.classList.contains("apexcharts-legend-series") || t.target.classList.contains("apexcharts-legend-text") || t.target.classList.contains("apexcharts-legend-marker"))) { var i = parseInt(t.target.getAttribute("rel"), 10) - 1, a = "true" === t.target.getAttribute("data:collapsed"), s = this.w.config.chart.events.legendClick; "function" == typeof s && s(this.ctx, i, this.w), this.ctx.events.fireEvent("legendClick", [this.ctx, i, this.w]); var r = this.w.config.legend.markers.onClick; "function" == typeof r && t.target.classList.contains("apexcharts-legend-marker") && (r(this.ctx, i, this.w), this.ctx.events.fireEvent("legendMarkerClick", [this.ctx, i, this.w])), "treemap" !== e.config.chart.type && "heatmap" !== e.config.chart.type && !this.isBarsDistributed && e.config.legend.onItemClick.toggleDataSeries && this.legendHelpers.toggleDataSeries(i, a) } } }]), t }(), ht = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w; var i = this.w; this.ev = this.w.config.chart.events, this.selectedClass = "apexcharts-selected", this.localeValues = this.w.globals.locale.toolbar, this.minX = i.globals.minX, this.maxX = i.globals.maxX } return r(t, [{ key: "createToolbar", value: function () { var t = this, e = this.w, i = function () { return document.createElement("div") }, a = i(); if (a.setAttribute("class", "apexcharts-toolbar"), a.style.top = e.config.chart.toolbar.offsetY + "px", a.style.right = 3 - e.config.chart.toolbar.offsetX + "px", e.globals.dom.elWrap.appendChild(a), this.elZoom = i(), this.elZoomIn = i(), this.elZoomOut = i(), this.elPan = i(), this.elSelection = i(), this.elZoomReset = i(), this.elMenuIcon = i(), this.elMenu = i(), this.elCustomIcons = [], this.t = e.config.chart.toolbar.tools, Array.isArray(this.t.customIcons)) for (var s = 0; s < this.t.customIcons.length; s++)this.elCustomIcons.push(i()); var r = [], o = function (i, a, s) { var o = i.toLowerCase(); t.t[o] && e.config.chart.zoom.enabled && r.push({ el: a, icon: "string" == typeof t.t[o] ? t.t[o] : s, title: t.localeValues[i], class: "apexcharts-".concat(o, "-icon") }) }; o("zoomIn", this.elZoomIn, '\n \n \n\n'), o("zoomOut", this.elZoomOut, '\n \n \n\n'); var n = function (i) { t.t[i] && e.config.chart[i].enabled && r.push({ el: "zoom" === i ? t.elZoom : t.elSelection, icon: "string" == typeof t.t[i] ? t.t[i] : "zoom" === i ? '\n \n \n \n' : '\n \n \n', title: t.localeValues["zoom" === i ? "selectionZoom" : "selection"], class: e.globals.isTouchDevice ? "apexcharts-element-hidden" : "apexcharts-".concat(i, "-icon") }) }; n("zoom"), n("selection"), this.t.pan && e.config.chart.zoom.enabled && r.push({ el: this.elPan, icon: "string" == typeof this.t.pan ? this.t.pan : '\n \n \n \n \n \n \n \n', title: this.localeValues.pan, class: e.globals.isTouchDevice ? "apexcharts-element-hidden" : "apexcharts-pan-icon" }), o("reset", this.elZoomReset, '\n \n \n'), this.t.download && r.push({ el: this.elMenuIcon, icon: "string" == typeof this.t.download ? this.t.download : '', title: this.localeValues.menu, class: "apexcharts-menu-icon" }); for (var l = 0; l < this.elCustomIcons.length; l++)r.push({ el: this.elCustomIcons[l], icon: this.t.customIcons[l].icon, title: this.t.customIcons[l].title, index: this.t.customIcons[l].index, class: "apexcharts-toolbar-custom-icon " + this.t.customIcons[l].class }); r.forEach((function (t, e) { t.index && x.moveIndexInArray(r, e, t.index) })); for (var h = 0; h < r.length; h++)m.setAttrs(r[h].el, { class: r[h].class, title: r[h].title }), r[h].el.innerHTML = r[h].icon, a.appendChild(r[h].el); this._createHamburgerMenu(a), e.globals.zoomEnabled ? this.elZoom.classList.add(this.selectedClass) : e.globals.panEnabled ? this.elPan.classList.add(this.selectedClass) : e.globals.selectionEnabled && this.elSelection.classList.add(this.selectedClass), this.addToolbarEventListeners() } }, { key: "_createHamburgerMenu", value: function (t) { this.elMenuItems = [], t.appendChild(this.elMenu), m.setAttrs(this.elMenu, { class: "apexcharts-menu" }); for (var e = [{ name: "exportSVG", title: this.localeValues.exportToSVG }, { name: "exportPNG", title: this.localeValues.exportToPNG }, { name: "exportCSV", title: this.localeValues.exportToCSV }], i = 0; i < e.length; i++)this.elMenuItems.push(document.createElement("div")), this.elMenuItems[i].innerHTML = e[i].title, m.setAttrs(this.elMenuItems[i], { class: "apexcharts-menu-item ".concat(e[i].name), title: e[i].title }), this.elMenu.appendChild(this.elMenuItems[i]) } }, { key: "addToolbarEventListeners", value: function () { var t = this; this.elZoomReset.addEventListener("click", this.handleZoomReset.bind(this)), this.elSelection.addEventListener("click", this.toggleZoomSelection.bind(this, "selection")), this.elZoom.addEventListener("click", this.toggleZoomSelection.bind(this, "zoom")), this.elZoomIn.addEventListener("click", this.handleZoomIn.bind(this)), this.elZoomOut.addEventListener("click", this.handleZoomOut.bind(this)), this.elPan.addEventListener("click", this.togglePanning.bind(this)), this.elMenuIcon.addEventListener("click", this.toggleMenu.bind(this)), this.elMenuItems.forEach((function (e) { e.classList.contains("exportSVG") ? e.addEventListener("click", t.handleDownload.bind(t, "svg")) : e.classList.contains("exportPNG") ? e.addEventListener("click", t.handleDownload.bind(t, "png")) : e.classList.contains("exportCSV") && e.addEventListener("click", t.handleDownload.bind(t, "csv")) })); for (var e = 0; e < this.t.customIcons.length; e++)this.elCustomIcons[e].addEventListener("click", this.t.customIcons[e].click.bind(this, this.ctx, this.ctx.w)) } }, { key: "toggleZoomSelection", value: function (t) { this.ctx.getSyncedCharts().forEach((function (e) { e.ctx.toolbar.toggleOtherControls(); var i = "selection" === t ? e.ctx.toolbar.elSelection : e.ctx.toolbar.elZoom, a = "selection" === t ? "selectionEnabled" : "zoomEnabled"; e.w.globals[a] = !e.w.globals[a], i.classList.contains(e.ctx.toolbar.selectedClass) ? i.classList.remove(e.ctx.toolbar.selectedClass) : i.classList.add(e.ctx.toolbar.selectedClass) })) } }, { key: "getToolbarIconsReference", value: function () { var t = this.w; this.elZoom || (this.elZoom = t.globals.dom.baseEl.querySelector(".apexcharts-zoom-icon")), this.elPan || (this.elPan = t.globals.dom.baseEl.querySelector(".apexcharts-pan-icon")), this.elSelection || (this.elSelection = t.globals.dom.baseEl.querySelector(".apexcharts-selection-icon")) } }, { key: "enableZoomPanFromToolbar", value: function (t) { this.toggleOtherControls(), "pan" === t ? this.w.globals.panEnabled = !0 : this.w.globals.zoomEnabled = !0; var e = "pan" === t ? this.elPan : this.elZoom, i = "pan" === t ? this.elZoom : this.elPan; e && e.classList.add(this.selectedClass), i && i.classList.remove(this.selectedClass) } }, { key: "togglePanning", value: function () { this.ctx.getSyncedCharts().forEach((function (t) { t.ctx.toolbar.toggleOtherControls(), t.w.globals.panEnabled = !t.w.globals.panEnabled, t.ctx.toolbar.elPan.classList.contains(t.ctx.toolbar.selectedClass) ? t.ctx.toolbar.elPan.classList.remove(t.ctx.toolbar.selectedClass) : t.ctx.toolbar.elPan.classList.add(t.ctx.toolbar.selectedClass) })) } }, { key: "toggleOtherControls", value: function () { var t = this, e = this.w; e.globals.panEnabled = !1, e.globals.zoomEnabled = !1, e.globals.selectionEnabled = !1, this.getToolbarIconsReference(), [this.elPan, this.elSelection, this.elZoom].forEach((function (e) { e && e.classList.remove(t.selectedClass) })) } }, { key: "handleZoomIn", value: function () { var t = this.w; t.globals.isRangeBar && (this.minX = t.globals.minY, this.maxX = t.globals.maxY); var e = (this.minX + this.maxX) / 2, i = (this.minX + e) / 2, a = (this.maxX + e) / 2, s = this._getNewMinXMaxX(i, a); t.globals.disableZoomIn || this.zoomUpdateOptions(s.minX, s.maxX) } }, { key: "handleZoomOut", value: function () { var t = this.w; if (t.globals.isRangeBar && (this.minX = t.globals.minY, this.maxX = t.globals.maxY), !("datetime" === t.config.xaxis.type && new Date(this.minX).getUTCFullYear() < 1e3)) { var e = (this.minX + this.maxX) / 2, i = this.minX - (e - this.minX), a = this.maxX - (e - this.maxX), s = this._getNewMinXMaxX(i, a); t.globals.disableZoomOut || this.zoomUpdateOptions(s.minX, s.maxX) } } }, { key: "_getNewMinXMaxX", value: function (t, e) { var i = this.w.config.xaxis.convertedCatToNumeric; return { minX: i ? Math.floor(t) : t, maxX: i ? Math.floor(e) : e } } }, { key: "zoomUpdateOptions", value: function (t, e) { var i = this.w; if (void 0 !== t || void 0 !== e) { if (!(i.config.xaxis.convertedCatToNumeric && (t < 1 && (t = 1, e = i.globals.dataPoints), e - t < 2))) { var a = { min: t, max: e }, s = this.getBeforeZoomRange(a); s && (a = s.xaxis); var r = { xaxis: a }, o = x.clone(i.globals.initialConfig.yaxis); i.config.chart.group || (r.yaxis = o), this.w.globals.zoomed = !0, this.ctx.updateHelpers._updateOptions(r, !1, this.w.config.chart.animations.dynamicAnimation.enabled), this.zoomCallback(a, o) } } else this.handleZoomReset() } }, { key: "zoomCallback", value: function (t, e) { "function" == typeof this.ev.zoomed && this.ev.zoomed(this.ctx, { xaxis: t, yaxis: e }) } }, { key: "getBeforeZoomRange", value: function (t, e) { var i = null; return "function" == typeof this.ev.beforeZoom && (i = this.ev.beforeZoom(this, { xaxis: t, yaxis: e })), i } }, { key: "toggleMenu", value: function () { var t = this; window.setTimeout((function () { t.elMenu.classList.contains("apexcharts-menu-open") ? t.elMenu.classList.remove("apexcharts-menu-open") : t.elMenu.classList.add("apexcharts-menu-open") }), 0) } }, { key: "handleDownload", value: function (t) { var e = this.w, i = new G(this.ctx); switch (t) { case "svg": i.exportToSVG(this.ctx); break; case "png": i.exportToPng(this.ctx); break; case "csv": i.exportToCSV({ series: e.config.series, columnDelimiter: e.config.chart.toolbar.export.csv.columnDelimiter }) } } }, { key: "handleZoomReset", value: function (t) { this.ctx.getSyncedCharts().forEach((function (t) { var e = t.w; if (e.globals.lastXAxis.min = e.globals.initialConfig.xaxis.min, e.globals.lastXAxis.max = e.globals.initialConfig.xaxis.max, t.updateHelpers.revertDefaultAxisMinMax(), "function" == typeof e.config.chart.events.beforeResetZoom) { var i = e.config.chart.events.beforeResetZoom(t, e); i && t.updateHelpers.revertDefaultAxisMinMax(i) } "function" == typeof e.config.chart.events.zoomed && t.ctx.toolbar.zoomCallback({ min: e.config.xaxis.min, max: e.config.xaxis.max }), e.globals.zoomed = !1; var a = t.ctx.series.emptyCollapsedSeries(x.clone(e.globals.initialSeries)); t.updateHelpers._updateSeries(a, e.config.chart.animations.dynamicAnimation.enabled) })) } }, { key: "destroy", value: function () { this.elZoom = null, this.elZoomIn = null, this.elZoomOut = null, this.elPan = null, this.elSelection = null, this.elZoomReset = null, this.elMenuIcon = null } }]), t }(), ct = function (t) { n(i, t); var e = d(i); function i(t) { var s; return a(this, i), (s = e.call(this, t)).ctx = t, s.w = t.w, s.dragged = !1, s.graphics = new m(s.ctx), s.eventList = ["mousedown", "mouseleave", "mousemove", "touchstart", "touchmove", "mouseup", "touchend"], s.clientX = 0, s.clientY = 0, s.startX = 0, s.endX = 0, s.dragX = 0, s.startY = 0, s.endY = 0, s.dragY = 0, s.moveDirection = "none", s } return r(i, [{ key: "init", value: function (t) { var e = this, i = t.xyRatios, a = this.w, s = this; this.xyRatios = i, this.zoomRect = this.graphics.drawRect(0, 0, 0, 0), this.selectionRect = this.graphics.drawRect(0, 0, 0, 0), this.gridRect = a.globals.dom.baseEl.querySelector(".apexcharts-grid"), this.zoomRect.node.classList.add("apexcharts-zoom-rect"), this.selectionRect.node.classList.add("apexcharts-selection-rect"), a.globals.dom.elGraphical.add(this.zoomRect), a.globals.dom.elGraphical.add(this.selectionRect), "x" === a.config.chart.selection.type ? this.slDraggableRect = this.selectionRect.draggable({ minX: 0, minY: 0, maxX: a.globals.gridWidth, maxY: a.globals.gridHeight }).on("dragmove", this.selectionDragging.bind(this, "dragging")) : "y" === a.config.chart.selection.type ? this.slDraggableRect = this.selectionRect.draggable({ minX: 0, maxX: a.globals.gridWidth }).on("dragmove", this.selectionDragging.bind(this, "dragging")) : this.slDraggableRect = this.selectionRect.draggable().on("dragmove", this.selectionDragging.bind(this, "dragging")), this.preselectedSelection(), this.hoverArea = a.globals.dom.baseEl.querySelector("".concat(a.globals.chartClass, " .apexcharts-svg")), this.hoverArea.classList.add("apexcharts-zoomable"), this.eventList.forEach((function (t) { e.hoverArea.addEventListener(t, s.svgMouseEvents.bind(s, i), { capture: !1, passive: !0 }) })) } }, { key: "destroy", value: function () { this.slDraggableRect && (this.slDraggableRect.draggable(!1), this.slDraggableRect.off(), this.selectionRect.off()), this.selectionRect = null, this.zoomRect = null, this.gridRect = null } }, { key: "svgMouseEvents", value: function (t, e) { var i = this.w, a = this, s = this.ctx.toolbar, r = i.globals.zoomEnabled ? i.config.chart.zoom.type : i.config.chart.selection.type, o = i.config.chart.toolbar.autoSelected; if (e.shiftKey ? (this.shiftWasPressed = !0, s.enableZoomPanFromToolbar("pan" === o ? "zoom" : "pan")) : this.shiftWasPressed && (s.enableZoomPanFromToolbar(o), this.shiftWasPressed = !1), e.target) { var n, l = e.target.classList; if (e.target.parentNode && null !== e.target.parentNode && (n = e.target.parentNode.classList), !(l.contains("apexcharts-selection-rect") || l.contains("apexcharts-legend-marker") || l.contains("apexcharts-legend-text") || n && n.contains("apexcharts-toolbar"))) { if (a.clientX = "touchmove" === e.type || "touchstart" === e.type ? e.touches[0].clientX : "touchend" === e.type ? e.changedTouches[0].clientX : e.clientX, a.clientY = "touchmove" === e.type || "touchstart" === e.type ? e.touches[0].clientY : "touchend" === e.type ? e.changedTouches[0].clientY : e.clientY, "mousedown" === e.type && 1 === e.which) { var h = a.gridRect.getBoundingClientRect(); a.startX = a.clientX - h.left, a.startY = a.clientY - h.top, a.dragged = !1, a.w.globals.mousedown = !0 } if (("mousemove" === e.type && 1 === e.which || "touchmove" === e.type) && (a.dragged = !0, i.globals.panEnabled ? (i.globals.selection = null, a.w.globals.mousedown && a.panDragging({ context: a, zoomtype: r, xyRatios: t })) : (a.w.globals.mousedown && i.globals.zoomEnabled || a.w.globals.mousedown && i.globals.selectionEnabled) && (a.selection = a.selectionDrawing({ context: a, zoomtype: r }))), "mouseup" === e.type || "touchend" === e.type || "mouseleave" === e.type) { var c = a.gridRect.getBoundingClientRect(); a.w.globals.mousedown && (a.endX = a.clientX - c.left, a.endY = a.clientY - c.top, a.dragX = Math.abs(a.endX - a.startX), a.dragY = Math.abs(a.endY - a.startY), (i.globals.zoomEnabled || i.globals.selectionEnabled) && a.selectionDrawn({ context: a, zoomtype: r }), i.globals.panEnabled && i.config.xaxis.convertedCatToNumeric && a.delayedPanScrolled()), i.globals.zoomEnabled && a.hideSelectionRect(this.selectionRect), a.dragged = !1, a.w.globals.mousedown = !1 } this.makeSelectionRectDraggable() } } } }, { key: "makeSelectionRectDraggable", value: function () { var t = this.w; if (this.selectionRect) { var e = this.selectionRect.node.getBoundingClientRect(); e.width > 0 && e.height > 0 && this.slDraggableRect.selectize({ points: "l, r", pointSize: 8, pointType: "rect" }).resize({ constraint: { minX: 0, minY: 0, maxX: t.globals.gridWidth, maxY: t.globals.gridHeight } }).on("resizing", this.selectionDragging.bind(this, "resizing")) } } }, { key: "preselectedSelection", value: function () { var t = this.w, e = this.xyRatios; if (!t.globals.zoomEnabled) if (void 0 !== t.globals.selection && null !== t.globals.selection) this.drawSelectionRect(t.globals.selection); else if (void 0 !== t.config.chart.selection.xaxis.min && void 0 !== t.config.chart.selection.xaxis.max) { var i = (t.config.chart.selection.xaxis.min - t.globals.minX) / e.xRatio, a = t.globals.gridWidth - (t.globals.maxX - t.config.chart.selection.xaxis.max) / e.xRatio - i; t.globals.isRangeBar && (i = (t.config.chart.selection.xaxis.min - t.globals.yAxisScale[0].niceMin) / e.invertedYRatio, a = (t.config.chart.selection.xaxis.max - t.config.chart.selection.xaxis.min) / e.invertedYRatio); var s = { x: i, y: 0, width: a, height: t.globals.gridHeight, translateX: 0, translateY: 0, selectionEnabled: !0 }; this.drawSelectionRect(s), this.makeSelectionRectDraggable(), "function" == typeof t.config.chart.events.selection && t.config.chart.events.selection(this.ctx, { xaxis: { min: t.config.chart.selection.xaxis.min, max: t.config.chart.selection.xaxis.max }, yaxis: {} }) } } }, { key: "drawSelectionRect", value: function (t) { var e = t.x, i = t.y, a = t.width, s = t.height, r = t.translateX, o = void 0 === r ? 0 : r, n = t.translateY, l = void 0 === n ? 0 : n, h = this.w, c = this.zoomRect, d = this.selectionRect; if (this.dragged || null !== h.globals.selection) { var g = { transform: "translate(" + o + ", " + l + ")" }; h.globals.zoomEnabled && this.dragged && (a < 0 && (a = 1), c.attr({ x: e, y: i, width: a, height: s, fill: h.config.chart.zoom.zoomedArea.fill.color, "fill-opacity": h.config.chart.zoom.zoomedArea.fill.opacity, stroke: h.config.chart.zoom.zoomedArea.stroke.color, "stroke-width": h.config.chart.zoom.zoomedArea.stroke.width, "stroke-opacity": h.config.chart.zoom.zoomedArea.stroke.opacity }), m.setAttrs(c.node, g)), h.globals.selectionEnabled && (d.attr({ x: e, y: i, width: a > 0 ? a : 0, height: s > 0 ? s : 0, fill: h.config.chart.selection.fill.color, "fill-opacity": h.config.chart.selection.fill.opacity, stroke: h.config.chart.selection.stroke.color, "stroke-width": h.config.chart.selection.stroke.width, "stroke-dasharray": h.config.chart.selection.stroke.dashArray, "stroke-opacity": h.config.chart.selection.stroke.opacity }), m.setAttrs(d.node, g)) } } }, { key: "hideSelectionRect", value: function (t) { t && t.attr({ x: 0, y: 0, width: 0, height: 0 }) } }, { key: "selectionDrawing", value: function (t) { var e = t.context, i = t.zoomtype, a = this.w, s = e, r = this.gridRect.getBoundingClientRect(), o = s.startX - 1, n = s.startY, l = !1, h = !1, c = s.clientX - r.left - o, d = s.clientY - r.top - n, g = {}; return Math.abs(c + o) > a.globals.gridWidth ? c = a.globals.gridWidth - o : s.clientX - r.left < 0 && (c = o), o > s.clientX - r.left && (l = !0, c = Math.abs(c)), n > s.clientY - r.top && (h = !0, d = Math.abs(d)), g = "x" === i ? { x: l ? o - c : o, y: 0, width: c, height: a.globals.gridHeight } : "y" === i ? { x: 0, y: h ? n - d : n, width: a.globals.gridWidth, height: d } : { x: l ? o - c : o, y: h ? n - d : n, width: c, height: d }, s.drawSelectionRect(g), s.selectionDragging("resizing"), g } }, { key: "selectionDragging", value: function (t, e) { var i = this, a = this.w, s = this.xyRatios, r = this.selectionRect, o = 0; "resizing" === t && (o = 30); var n = function (t) { return parseFloat(r.node.getAttribute(t)) }, l = { x: n("x"), y: n("y"), width: n("width"), height: n("height") }; a.globals.selection = l, "function" == typeof a.config.chart.events.selection && a.globals.selectionEnabled && (clearTimeout(this.w.globals.selectionResizeTimer), this.w.globals.selectionResizeTimer = window.setTimeout((function () { var t, e, o, n, l = i.gridRect.getBoundingClientRect(), h = r.node.getBoundingClientRect(); a.globals.isRangeBar ? (t = a.globals.yAxisScale[0].niceMin + (h.left - l.left) * s.invertedYRatio, e = a.globals.yAxisScale[0].niceMin + (h.right - l.left) * s.invertedYRatio, o = 0, n = 1) : (t = a.globals.xAxisScale.niceMin + (h.left - l.left) * s.xRatio, e = a.globals.xAxisScale.niceMin + (h.right - l.left) * s.xRatio, o = a.globals.yAxisScale[0].niceMin + (l.bottom - h.bottom) * s.yRatio[0], n = a.globals.yAxisScale[0].niceMax - (h.top - l.top) * s.yRatio[0]); var c = { xaxis: { min: t, max: e }, yaxis: { min: o, max: n } }; a.config.chart.events.selection(i.ctx, c), a.config.chart.brush.enabled && void 0 !== a.config.chart.events.brushScrolled && a.config.chart.events.brushScrolled(i.ctx, c) }), o)) } }, { key: "selectionDrawn", value: function (t) { var e = t.context, i = t.zoomtype, a = this.w, s = e, r = this.xyRatios, o = this.ctx.toolbar; if (s.startX > s.endX) { var n = s.startX; s.startX = s.endX, s.endX = n } if (s.startY > s.endY) { var l = s.startY; s.startY = s.endY, s.endY = l } var h = void 0, c = void 0; a.globals.isRangeBar ? (h = a.globals.yAxisScale[0].niceMin + s.startX * r.invertedYRatio, c = a.globals.yAxisScale[0].niceMin + s.endX * r.invertedYRatio) : (h = a.globals.xAxisScale.niceMin + s.startX * r.xRatio, c = a.globals.xAxisScale.niceMin + s.endX * r.xRatio); var d = [], g = []; if (a.config.yaxis.forEach((function (t, e) { if (a.globals.seriesYAxisMap[e].length > 0) { var i = a.globals.seriesYAxisMap[e][0]; d.push(a.globals.yAxisScale[e].niceMax - r.yRatio[i] * s.startY), g.push(a.globals.yAxisScale[e].niceMax - r.yRatio[i] * s.endY) } })), s.dragged && (s.dragX > 10 || s.dragY > 10) && h !== c) if (a.globals.zoomEnabled) { var u = x.clone(a.globals.initialConfig.yaxis), p = x.clone(a.globals.initialConfig.xaxis); if (a.globals.zoomed = !0, a.config.xaxis.convertedCatToNumeric && (h = Math.floor(h), c = Math.floor(c), h < 1 && (h = 1, c = a.globals.dataPoints), c - h < 2 && (c = h + 1)), "xy" !== i && "x" !== i || (p = { min: h, max: c }), "xy" !== i && "y" !== i || u.forEach((function (t, e) { u[e].min = g[e], u[e].max = d[e] })), o) { var f = o.getBeforeZoomRange(p, u); f && (p = f.xaxis ? f.xaxis : p, u = f.yaxis ? f.yaxis : u) } var b = { xaxis: p }; a.config.chart.group || (b.yaxis = u), s.ctx.updateHelpers._updateOptions(b, !1, s.w.config.chart.animations.dynamicAnimation.enabled), "function" == typeof a.config.chart.events.zoomed && o.zoomCallback(p, u) } else if (a.globals.selectionEnabled) { var v, m = null; v = { min: h, max: c }, "xy" !== i && "y" !== i || (m = x.clone(a.config.yaxis)).forEach((function (t, e) { m[e].min = g[e], m[e].max = d[e] })), a.globals.selection = s.selection, "function" == typeof a.config.chart.events.selection && a.config.chart.events.selection(s.ctx, { xaxis: v, yaxis: m }) } } }, { key: "panDragging", value: function (t) { var e = t.context, i = this.w, a = e; if (void 0 !== i.globals.lastClientPosition.x) { var s = i.globals.lastClientPosition.x - a.clientX, r = i.globals.lastClientPosition.y - a.clientY; Math.abs(s) > Math.abs(r) && s > 0 ? this.moveDirection = "left" : Math.abs(s) > Math.abs(r) && s < 0 ? this.moveDirection = "right" : Math.abs(r) > Math.abs(s) && r > 0 ? this.moveDirection = "up" : Math.abs(r) > Math.abs(s) && r < 0 && (this.moveDirection = "down") } i.globals.lastClientPosition = { x: a.clientX, y: a.clientY }; var o = i.globals.isRangeBar ? i.globals.minY : i.globals.minX, n = i.globals.isRangeBar ? i.globals.maxY : i.globals.maxX; i.config.xaxis.convertedCatToNumeric || a.panScrolled(o, n) } }, { key: "delayedPanScrolled", value: function () { var t = this.w, e = t.globals.minX, i = t.globals.maxX, a = (t.globals.maxX - t.globals.minX) / 2; "left" === this.moveDirection ? (e = t.globals.minX + a, i = t.globals.maxX + a) : "right" === this.moveDirection && (e = t.globals.minX - a, i = t.globals.maxX - a), e = Math.floor(e), i = Math.floor(i), this.updateScrolledChart({ xaxis: { min: e, max: i } }, e, i) } }, { key: "panScrolled", value: function (t, e) { var i = this.w, a = this.xyRatios, s = x.clone(i.globals.initialConfig.yaxis), r = a.xRatio, o = i.globals.minX, n = i.globals.maxX; i.globals.isRangeBar && (r = a.invertedYRatio, o = i.globals.minY, n = i.globals.maxY), "left" === this.moveDirection ? (t = o + i.globals.gridWidth / 15 * r, e = n + i.globals.gridWidth / 15 * r) : "right" === this.moveDirection && (t = o - i.globals.gridWidth / 15 * r, e = n - i.globals.gridWidth / 15 * r), i.globals.isRangeBar || (t < i.globals.initialMinX || e > i.globals.initialMaxX) && (t = o, e = n); var l = { xaxis: { min: t, max: e } }; i.config.chart.group || (l.yaxis = s), this.updateScrolledChart(l, t, e) } }, { key: "updateScrolledChart", value: function (t, e, i) { var a = this.w; this.ctx.updateHelpers._updateOptions(t, !1, !1), "function" == typeof a.config.chart.events.scrolled && a.config.chart.events.scrolled(this.ctx, { xaxis: { min: e, max: i } }) } }]), i }(ht), dt = function () { function t(e) { a(this, t), this.w = e.w, this.ttCtx = e, this.ctx = e.ctx } return r(t, [{ key: "getNearestValues", value: function (t) { var e = t.hoverArea, i = t.elGrid, a = t.clientX, s = t.clientY, r = this.w, o = i.getBoundingClientRect(), n = o.width, l = o.height, h = n / (r.globals.dataPoints - 1), c = l / r.globals.dataPoints, d = this.hasBars(); !r.globals.comboCharts && !d || r.config.xaxis.convertedCatToNumeric || (h = n / r.globals.dataPoints); var g = a - o.left - r.globals.barPadForNumericAxis, u = s - o.top; g < 0 || u < 0 || g > n || u > l ? (e.classList.remove("hovering-zoom"), e.classList.remove("hovering-pan")) : r.globals.zoomEnabled ? (e.classList.remove("hovering-pan"), e.classList.add("hovering-zoom")) : r.globals.panEnabled && (e.classList.remove("hovering-zoom"), e.classList.add("hovering-pan")); var p = Math.round(g / h), f = Math.floor(u / c); d && !r.config.xaxis.convertedCatToNumeric && (p = Math.ceil(g / h), p -= 1); var b = null, v = null, m = r.globals.seriesXvalues.map((function (t) { return t.filter((function (t) { return x.isNumber(t) })) })), y = r.globals.seriesYvalues.map((function (t) { return t.filter((function (t) { return x.isNumber(t) })) })); if (r.globals.isXNumeric) { var w = this.ttCtx.getElGrid().getBoundingClientRect(), k = g * (w.width / n), A = u * (w.height / l); b = (v = this.closestInMultiArray(k, A, m, y)).index, p = v.j, null !== b && (m = r.globals.seriesXvalues[b], p = (v = this.closestInArray(k, m)).index) } return r.globals.capturedSeriesIndex = null === b ? -1 : b, (!p || p < 1) && (p = 0), r.globals.isBarHorizontal ? r.globals.capturedDataPointIndex = f : r.globals.capturedDataPointIndex = p, { capturedSeries: b, j: r.globals.isBarHorizontal ? f : p, hoverX: g, hoverY: u } } }, { key: "closestInMultiArray", value: function (t, e, i, a) { var s = this.w, r = 0, o = null, n = -1; s.globals.series.length > 1 ? r = this.getFirstActiveXArray(i) : o = 0; var l = i[r][0], h = Math.abs(t - l); if (i.forEach((function (e) { e.forEach((function (e, i) { var a = Math.abs(t - e); a <= h && (h = a, n = i) })) })), -1 !== n) { var c = a[r][n], d = Math.abs(e - c); o = r, a.forEach((function (t, i) { var a = Math.abs(e - t[n]); a <= d && (d = a, o = i) })) } return { index: o, j: n } } }, { key: "getFirstActiveXArray", value: function (t) { for (var e = this.w, i = 0, a = t.map((function (t, e) { return t.length > 0 ? e : -1 })), s = 0; s < a.length; s++)if (-1 !== a[s] && -1 === e.globals.collapsedSeriesIndices.indexOf(s) && -1 === e.globals.ancillaryCollapsedSeriesIndices.indexOf(s)) { i = a[s]; break } return i } }, { key: "closestInArray", value: function (t, e) { for (var i = e[0], a = null, s = Math.abs(t - i), r = 0; r < e.length; r++) { var o = Math.abs(t - e[r]); o < s && (s = o, a = r) } return { index: a } } }, { key: "isXoverlap", value: function (t) { var e = [], i = this.w.globals.seriesX.filter((function (t) { return void 0 !== t[0] })); if (i.length > 0) for (var a = 0; a < i.length - 1; a++)void 0 !== i[a][t] && void 0 !== i[a + 1][t] && i[a][t] !== i[a + 1][t] && e.push("unEqual"); return 0 === e.length } }, { key: "isInitialSeriesSameLen", value: function () { for (var t = !0, e = this.w.globals.initialSeries, i = 0; i < e.length - 1; i++)if (e[i].data.length !== e[i + 1].data.length) { t = !1; break } return t } }, { key: "getBarsHeight", value: function (t) { return u(t).reduce((function (t, e) { return t + e.getBBox().height }), 0) } }, { key: "getElMarkers", value: function (t) { return "number" == typeof t ? this.w.globals.dom.baseEl.querySelectorAll(".apexcharts-series[data\\:realIndex='".concat(t, "'] .apexcharts-series-markers-wrap > *")) : this.w.globals.dom.baseEl.querySelectorAll(".apexcharts-series-markers-wrap > *") } }, { key: "getAllMarkers", value: function () { var t = this.w.globals.dom.baseEl.querySelectorAll(".apexcharts-series-markers-wrap"); (t = u(t)).sort((function (t, e) { var i = Number(t.getAttribute("data:realIndex")), a = Number(e.getAttribute("data:realIndex")); return a < i ? 1 : a > i ? -1 : 0 })); var e = []; return t.forEach((function (t) { e.push(t.querySelector(".apexcharts-marker")) })), e } }, { key: "hasMarkers", value: function (t) { return this.getElMarkers(t).length > 0 } }, { key: "getElBars", value: function () { return this.w.globals.dom.baseEl.querySelectorAll(".apexcharts-bar-series, .apexcharts-candlestick-series, .apexcharts-boxPlot-series, .apexcharts-rangebar-series") } }, { key: "hasBars", value: function () { return this.getElBars().length > 0 } }, { key: "getHoverMarkerSize", value: function (t) { var e = this.w, i = e.config.markers.hover.size; return void 0 === i && (i = e.globals.markers.size[t] + e.config.markers.hover.sizeOffset), i } }, { key: "toggleAllTooltipSeriesGroups", value: function (t) { var e = this.w, i = this.ttCtx; 0 === i.allTooltipSeriesGroups.length && (i.allTooltipSeriesGroups = e.globals.dom.baseEl.querySelectorAll(".apexcharts-tooltip-series-group")); for (var a = i.allTooltipSeriesGroups, s = 0; s < a.length; s++)"enable" === t ? (a[s].classList.add("apexcharts-active"), a[s].style.display = e.config.tooltip.items.display) : (a[s].classList.remove("apexcharts-active"), a[s].style.display = "none") } }]), t }(), gt = function () { function t(e) { a(this, t), this.w = e.w, this.ctx = e.ctx, this.ttCtx = e, this.tooltipUtil = new dt(e) } return r(t, [{ key: "drawSeriesTexts", value: function (t) { var e = t.shared, i = void 0 === e || e, a = t.ttItems, s = t.i, r = void 0 === s ? 0 : s, o = t.j, n = void 0 === o ? null : o, l = t.y1, h = t.y2, c = t.e, d = this.w; void 0 !== d.config.tooltip.custom ? this.handleCustomTooltip({ i: r, j: n, y1: l, y2: h, w: d }) : this.toggleActiveInactiveSeries(i); var g = this.getValuesToPrint({ i: r, j: n }); this.printLabels({ i: r, j: n, values: g, ttItems: a, shared: i, e: c }); var u = this.ttCtx.getElTooltip(); this.ttCtx.tooltipRect.ttWidth = u.getBoundingClientRect().width, this.ttCtx.tooltipRect.ttHeight = u.getBoundingClientRect().height } }, { key: "printLabels", value: function (t) { var i, a = this, s = t.i, r = t.j, o = t.values, n = t.ttItems, l = t.shared, h = t.e, c = this.w, d = [], g = function (t) { return c.globals.seriesGoals[t] && c.globals.seriesGoals[t][r] && Array.isArray(c.globals.seriesGoals[t][r]) }, u = o.xVal, p = o.zVal, f = o.xAxisTTVal, x = "", b = c.globals.colors[s]; null !== r && c.config.plotOptions.bar.distributed && (b = c.globals.colors[r]); for (var v = function (t, o) { var v = a.getFormatters(s); x = a.getSeriesName({ fn: v.yLbTitleFormatter, index: s, seriesIndex: s, j: r }), "treemap" === c.config.chart.type && (x = v.yLbTitleFormatter(String(c.config.series[s].data[r].x), { series: c.globals.series, seriesIndex: s, dataPointIndex: r, w: c })); var m = c.config.tooltip.inverseOrder ? o : t; if (c.globals.axisCharts) { var y = function (t) { var e, i, a, s; return c.globals.isRangeData ? v.yLbFormatter(null === (e = c.globals.seriesRangeStart) || void 0 === e || null === (i = e[t]) || void 0 === i ? void 0 : i[r], { series: c.globals.seriesRangeStart, seriesIndex: t, dataPointIndex: r, w: c }) + " - " + v.yLbFormatter(null === (a = c.globals.seriesRangeEnd) || void 0 === a || null === (s = a[t]) || void 0 === s ? void 0 : s[r], { series: c.globals.seriesRangeEnd, seriesIndex: t, dataPointIndex: r, w: c }) : v.yLbFormatter(c.globals.series[t][r], { series: c.globals.series, seriesIndex: t, dataPointIndex: r, w: c }) }; if (l) v = a.getFormatters(m), x = a.getSeriesName({ fn: v.yLbTitleFormatter, index: m, seriesIndex: s, j: r }), b = c.globals.colors[m], i = y(m), g(m) && (d = c.globals.seriesGoals[m][r].map((function (t) { return { attrs: t, val: v.yLbFormatter(t.value, { seriesIndex: m, dataPointIndex: r, w: c }) } }))); else { var w, k = null == h || null === (w = h.target) || void 0 === w ? void 0 : w.getAttribute("fill"); k && (b = -1 !== k.indexOf("url") ? document.querySelector(k.substr(4).slice(0, -1)).childNodes[0].getAttribute("stroke") : k), i = y(s), g(s) && Array.isArray(c.globals.seriesGoals[s][r]) && (d = c.globals.seriesGoals[s][r].map((function (t) { return { attrs: t, val: v.yLbFormatter(t.value, { seriesIndex: s, dataPointIndex: r, w: c }) } }))) } } null === r && (i = v.yLbFormatter(c.globals.series[s], e(e({}, c), {}, { seriesIndex: s, dataPointIndex: s }))), a.DOMHandling({ i: s, t: m, j: r, ttItems: n, values: { val: i, goalVals: d, xVal: u, xAxisTTVal: f, zVal: p }, seriesName: x, shared: l, pColor: b }) }, m = 0, y = c.globals.series.length - 1; m < c.globals.series.length; m++, y--)v(m, y) } }, { key: "getFormatters", value: function (t) { var e, i = this.w, a = i.globals.yLabelFormatters[t]; return void 0 !== i.globals.ttVal ? Array.isArray(i.globals.ttVal) ? (a = i.globals.ttVal[t] && i.globals.ttVal[t].formatter, e = i.globals.ttVal[t] && i.globals.ttVal[t].title && i.globals.ttVal[t].title.formatter) : (a = i.globals.ttVal.formatter, "function" == typeof i.globals.ttVal.title.formatter && (e = i.globals.ttVal.title.formatter)) : e = i.config.tooltip.y.title.formatter, "function" != typeof a && (a = i.globals.yLabelFormatters[0] ? i.globals.yLabelFormatters[0] : function (t) { return t }), "function" != typeof e && (e = function (t) { return t }), { yLbFormatter: a, yLbTitleFormatter: e } } }, { key: "getSeriesName", value: function (t) { var e = t.fn, i = t.index, a = t.seriesIndex, s = t.j, r = this.w; return e(String(r.globals.seriesNames[i]), { series: r.globals.series, seriesIndex: a, dataPointIndex: s, w: r }) } }, { key: "DOMHandling", value: function (t) { t.i; var e = t.t, i = t.j, a = t.ttItems, s = t.values, r = t.seriesName, o = t.shared, n = t.pColor, l = this.w, h = this.ttCtx, c = s.val, d = s.goalVals, g = s.xVal, u = s.xAxisTTVal, p = s.zVal, f = null; f = a[e].children, l.config.tooltip.fillSeriesColor && (a[e].style.backgroundColor = n, f[0].style.display = "none"), h.showTooltipTitle && (null === h.tooltipTitle && (h.tooltipTitle = l.globals.dom.baseEl.querySelector(".apexcharts-tooltip-title")), h.tooltipTitle.innerHTML = g), h.isXAxisTooltipEnabled && (h.xaxisTooltipText.innerHTML = "" !== u ? u : g); var x = a[e].querySelector(".apexcharts-tooltip-text-y-label"); x && (x.innerHTML = r || ""); var b = a[e].querySelector(".apexcharts-tooltip-text-y-value"); b && (b.innerHTML = void 0 !== c ? c : ""), f[0] && f[0].classList.contains("apexcharts-tooltip-marker") && (l.config.tooltip.marker.fillColors && Array.isArray(l.config.tooltip.marker.fillColors) && (n = l.config.tooltip.marker.fillColors[e]), f[0].style.backgroundColor = n), l.config.tooltip.marker.show || (f[0].style.display = "none"); var v = a[e].querySelector(".apexcharts-tooltip-text-goals-label"), m = a[e].querySelector(".apexcharts-tooltip-text-goals-value"); if (d.length && l.globals.seriesGoals[e]) { var y = function () { var t = "
", e = "
"; d.forEach((function (i, a) { t += '
').concat(i.attrs.name, "
"), e += "
".concat(i.val, "
") })), v.innerHTML = t + "
", m.innerHTML = e + "
" }; o ? l.globals.seriesGoals[e][i] && Array.isArray(l.globals.seriesGoals[e][i]) ? y() : (v.innerHTML = "", m.innerHTML = "") : y() } else v.innerHTML = "", m.innerHTML = ""; null !== p && (a[e].querySelector(".apexcharts-tooltip-text-z-label").innerHTML = l.config.tooltip.z.title, a[e].querySelector(".apexcharts-tooltip-text-z-value").innerHTML = void 0 !== p ? p : ""); if (o && f[0]) { if (l.config.tooltip.hideEmptySeries) { var w = a[e].querySelector(".apexcharts-tooltip-marker"), k = a[e].querySelector(".apexcharts-tooltip-text"); 0 == parseFloat(c) ? (w.style.display = "none", k.style.display = "none") : (w.style.display = "block", k.style.display = "block") } null == c || l.globals.ancillaryCollapsedSeriesIndices.indexOf(e) > -1 || l.globals.collapsedSeriesIndices.indexOf(e) > -1 ? f[0].parentNode.style.display = "none" : f[0].parentNode.style.display = l.config.tooltip.items.display } } }, { key: "toggleActiveInactiveSeries", value: function (t) { var e = this.w; if (t) this.tooltipUtil.toggleAllTooltipSeriesGroups("enable"); else { this.tooltipUtil.toggleAllTooltipSeriesGroups("disable"); var i = e.globals.dom.baseEl.querySelector(".apexcharts-tooltip-series-group"); i && (i.classList.add("apexcharts-active"), i.style.display = e.config.tooltip.items.display) } } }, { key: "getValuesToPrint", value: function (t) { var e = t.i, i = t.j, a = this.w, s = this.ctx.series.filteredSeriesX(), r = "", o = "", n = null, l = null, h = { series: a.globals.series, seriesIndex: e, dataPointIndex: i, w: a }, c = a.globals.ttZFormatter; null === i ? l = a.globals.series[e] : a.globals.isXNumeric && "treemap" !== a.config.chart.type ? (r = s[e][i], 0 === s[e].length && (r = s[this.tooltipUtil.getFirstActiveXArray(s)][i])) : r = void 0 !== a.globals.labels[i] ? a.globals.labels[i] : ""; var d = r; a.globals.isXNumeric && "datetime" === a.config.xaxis.type ? r = new S(this.ctx).xLabelFormat(a.globals.ttKeyFormatter, d, d, { i: void 0, dateFormatter: new A(this.ctx).formatDate, w: this.w }) : r = a.globals.isBarHorizontal ? a.globals.yLabelFormatters[0](d, h) : a.globals.xLabelFormatter(d, h); return void 0 !== a.config.tooltip.x.formatter && (r = a.globals.ttKeyFormatter(d, h)), a.globals.seriesZ.length > 0 && a.globals.seriesZ[e].length > 0 && (n = c(a.globals.seriesZ[e][i], a)), o = "function" == typeof a.config.xaxis.tooltip.formatter ? a.globals.xaxisTooltipFormatter(d, h) : r, { val: Array.isArray(l) ? l.join(" ") : l, xVal: Array.isArray(r) ? r.join(" ") : r, xAxisTTVal: Array.isArray(o) ? o.join(" ") : o, zVal: n } } }, { key: "handleCustomTooltip", value: function (t) { var e = t.i, i = t.j, a = t.y1, s = t.y2, r = t.w, o = this.ttCtx.getElTooltip(), n = r.config.tooltip.custom; Array.isArray(n) && n[e] && (n = n[e]), o.innerHTML = n({ ctx: this.ctx, series: r.globals.series, seriesIndex: e, dataPointIndex: i, y1: a, y2: s, w: r }) } }]), t }(), ut = function () { function t(e) { a(this, t), this.ttCtx = e, this.ctx = e.ctx, this.w = e.w } return r(t, [{ key: "moveXCrosshairs", value: function (t) { var e = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : null, i = this.ttCtx, a = this.w, s = i.getElXCrosshairs(), r = t - i.xcrosshairsWidth / 2, o = a.globals.labels.slice().length; if (null !== e && (r = a.globals.gridWidth / o * e), null === s || a.globals.isBarHorizontal || (s.setAttribute("x", r), s.setAttribute("x1", r), s.setAttribute("x2", r), s.setAttribute("y2", a.globals.gridHeight), s.classList.add("apexcharts-active")), r < 0 && (r = 0), r > a.globals.gridWidth && (r = a.globals.gridWidth), i.isXAxisTooltipEnabled) { var n = r; "tickWidth" !== a.config.xaxis.crosshairs.width && "barWidth" !== a.config.xaxis.crosshairs.width || (n = r + i.xcrosshairsWidth / 2), this.moveXAxisTooltip(n) } } }, { key: "moveYCrosshairs", value: function (t) { var e = this.ttCtx; null !== e.ycrosshairs && m.setAttrs(e.ycrosshairs, { y1: t, y2: t }), null !== e.ycrosshairsHidden && m.setAttrs(e.ycrosshairsHidden, { y1: t, y2: t }) } }, { key: "moveXAxisTooltip", value: function (t) { var e = this.w, i = this.ttCtx; if (null !== i.xaxisTooltip && 0 !== i.xcrosshairsWidth) { i.xaxisTooltip.classList.add("apexcharts-active"); var a = i.xaxisOffY + e.config.xaxis.tooltip.offsetY + e.globals.translateY + 1 + e.config.xaxis.offsetY; if (t -= i.xaxisTooltip.getBoundingClientRect().width / 2, !isNaN(t)) { t += e.globals.translateX; var s; s = new m(this.ctx).getTextRects(i.xaxisTooltipText.innerHTML), i.xaxisTooltipText.style.minWidth = s.width + "px", i.xaxisTooltip.style.left = t + "px", i.xaxisTooltip.style.top = a + "px" } } } }, { key: "moveYAxisTooltip", value: function (t) { var e = this.w, i = this.ttCtx; null === i.yaxisTTEls && (i.yaxisTTEls = e.globals.dom.baseEl.querySelectorAll(".apexcharts-yaxistooltip")); var a = parseInt(i.ycrosshairsHidden.getAttribute("y1"), 10), s = e.globals.translateY + a, r = i.yaxisTTEls[t].getBoundingClientRect().height, o = e.globals.translateYAxisX[t] - 2; e.config.yaxis[t].opposite && (o -= 26), s -= r / 2, -1 === e.globals.ignoreYAxisIndexes.indexOf(t) ? (i.yaxisTTEls[t].classList.add("apexcharts-active"), i.yaxisTTEls[t].style.top = s + "px", i.yaxisTTEls[t].style.left = o + e.config.yaxis[t].tooltip.offsetX + "px") : i.yaxisTTEls[t].classList.remove("apexcharts-active") } }, { key: "moveTooltip", value: function (t, e) { var i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : null, a = this.w, s = this.ttCtx, r = s.getElTooltip(), o = s.tooltipRect, n = null !== i ? parseFloat(i) : 1, l = parseFloat(t) + n + 5, h = parseFloat(e) + n / 2; if (l > a.globals.gridWidth / 2 && (l = l - o.ttWidth - n - 10), l > a.globals.gridWidth - o.ttWidth - 10 && (l = a.globals.gridWidth - o.ttWidth), l < -20 && (l = -20), a.config.tooltip.followCursor) { var c = s.getElGrid().getBoundingClientRect(); (l = s.e.clientX - c.left) > a.globals.gridWidth / 2 && (l -= s.tooltipRect.ttWidth), (h = s.e.clientY + a.globals.translateY - c.top) > a.globals.gridHeight / 2 && (h -= s.tooltipRect.ttHeight) } else a.globals.isBarHorizontal || o.ttHeight / 2 + h > a.globals.gridHeight && (h = a.globals.gridHeight - o.ttHeight + a.globals.translateY); isNaN(l) || (l += a.globals.translateX, r.style.left = l + "px", r.style.top = h + "px") } }, { key: "moveMarkers", value: function (t, e) { var i = this.w, a = this.ttCtx; if (i.globals.markers.size[t] > 0) for (var s = i.globals.dom.baseEl.querySelectorAll(" .apexcharts-series[data\\:realIndex='".concat(t, "'] .apexcharts-marker")), r = 0; r < s.length; r++)parseInt(s[r].getAttribute("rel"), 10) === e && (a.marker.resetPointsSize(), a.marker.enlargeCurrentPoint(e, s[r])); else a.marker.resetPointsSize(), this.moveDynamicPointOnHover(e, t) } }, { key: "moveDynamicPointOnHover", value: function (t, e) { var i, a, s = this.w, r = this.ttCtx, o = s.globals.pointsArray, n = r.tooltipUtil.getHoverMarkerSize(e), l = s.config.series[e].type; if (!l || "column" !== l && "candlestick" !== l && "boxPlot" !== l) { i = o[e][t][0], a = o[e][t][1] ? o[e][t][1] : 0; var h = s.globals.dom.baseEl.querySelector(".apexcharts-series[data\\:realIndex='".concat(e, "'] .apexcharts-series-markers circle")); h && a < s.globals.gridHeight && a > 0 && (h.setAttribute("r", n), h.setAttribute("cx", i), h.setAttribute("cy", a)), this.moveXCrosshairs(i), r.fixedTooltip || this.moveTooltip(i, a, n) } } }, { key: "moveDynamicPointsOnHover", value: function (t) { var e, i = this.ttCtx, a = i.w, s = 0, r = 0, o = a.globals.pointsArray; e = new W(this.ctx).getActiveConfigSeriesIndex("asc", ["line", "area", "scatter", "bubble"]); var n = i.tooltipUtil.getHoverMarkerSize(e); o[e] && (s = o[e][t][0], r = o[e][t][1]); var l = i.tooltipUtil.getAllMarkers(); if (null !== l) for (var h = 0; h < a.globals.series.length; h++) { var c = o[h]; if (a.globals.comboCharts && void 0 === c && l.splice(h, 0, null), c && c.length) { var d = o[h][t][1], g = void 0; if (l[h].setAttribute("cx", s), "rangeArea" === a.config.chart.type && !a.globals.comboCharts) { var u = t + a.globals.series[h].length; g = o[h][u][1], d -= Math.abs(d - g) / 2 } null !== d && !isNaN(d) && d < a.globals.gridHeight + n && d + n > 0 ? (l[h] && l[h].setAttribute("r", n), l[h] && l[h].setAttribute("cy", d)) : l[h] && l[h].setAttribute("r", 0) } } this.moveXCrosshairs(s), i.fixedTooltip || this.moveTooltip(s, r || a.globals.gridHeight, n) } }, { key: "moveStickyTooltipOverBars", value: function (t, e) { var i = this.w, a = this.ttCtx, s = i.globals.columnSeries ? i.globals.columnSeries.length : i.globals.series.length, r = s >= 2 && s % 2 == 0 ? Math.floor(s / 2) : Math.floor(s / 2) + 1; i.globals.isBarHorizontal && (r = new W(this.ctx).getActiveConfigSeriesIndex("desc") + 1); var o = i.globals.dom.baseEl.querySelector(".apexcharts-bar-series .apexcharts-series[rel='".concat(r, "'] path[j='").concat(t, "'], .apexcharts-candlestick-series .apexcharts-series[rel='").concat(r, "'] path[j='").concat(t, "'], .apexcharts-boxPlot-series .apexcharts-series[rel='").concat(r, "'] path[j='").concat(t, "'], .apexcharts-rangebar-series .apexcharts-series[rel='").concat(r, "'] path[j='").concat(t, "']")); o || "number" != typeof e || (o = i.globals.dom.baseEl.querySelector(".apexcharts-bar-series .apexcharts-series[data\\:realIndex='".concat(e, "'] path[j='").concat(t, "'],\n .apexcharts-candlestick-series .apexcharts-series[data\\:realIndex='").concat(e, "'] path[j='").concat(t, "'],\n .apexcharts-boxPlot-series .apexcharts-series[data\\:realIndex='").concat(e, "'] path[j='").concat(t, "'],\n .apexcharts-rangebar-series .apexcharts-series[data\\:realIndex='").concat(e, "'] path[j='").concat(t, "']"))); var n = o ? parseFloat(o.getAttribute("cx")) : 0, l = o ? parseFloat(o.getAttribute("cy")) : 0, h = o ? parseFloat(o.getAttribute("barWidth")) : 0, c = a.getElGrid().getBoundingClientRect(), d = o && (o.classList.contains("apexcharts-candlestick-area") || o.classList.contains("apexcharts-boxPlot-area")); i.globals.isXNumeric ? (o && !d && (n -= s % 2 != 0 ? h / 2 : 0), o && d && i.globals.comboCharts && (n -= h / 2)) : i.globals.isBarHorizontal || (n = a.xAxisTicksPositions[t - 1] + a.dataPointsDividedWidth / 2, isNaN(n) && (n = a.xAxisTicksPositions[t] - a.dataPointsDividedWidth / 2)), i.globals.isBarHorizontal ? l -= a.tooltipRect.ttHeight : i.config.tooltip.followCursor ? l = a.e.clientY - c.top - a.tooltipRect.ttHeight / 2 : l + a.tooltipRect.ttHeight + 15 > i.globals.gridHeight && (l = i.globals.gridHeight), i.globals.isBarHorizontal || this.moveXCrosshairs(n), a.fixedTooltip || this.moveTooltip(n, l || i.globals.gridHeight) } }]), t }(), pt = function () { function t(e) { a(this, t), this.w = e.w, this.ttCtx = e, this.ctx = e.ctx, this.tooltipPosition = new ut(e) } return r(t, [{ key: "drawDynamicPoints", value: function () { var t = this.w, e = new m(this.ctx), i = new D(this.ctx), a = t.globals.dom.baseEl.querySelectorAll(".apexcharts-series"); a = u(a), t.config.chart.stacked && a.sort((function (t, e) { return parseFloat(t.getAttribute("data:realIndex")) - parseFloat(e.getAttribute("data:realIndex")) })); for (var s = 0; s < a.length; s++) { var r = a[s].querySelector(".apexcharts-series-markers-wrap"); if (null !== r) { var o = void 0, n = "apexcharts-marker w".concat((Math.random() + 1).toString(36).substring(4)); "line" !== t.config.chart.type && "area" !== t.config.chart.type || t.globals.comboCharts || t.config.tooltip.intersect || (n += " no-pointer-events"); var l = i.getMarkerConfig({ cssClass: n, seriesIndex: Number(r.getAttribute("data:realIndex")) }); (o = e.drawMarker(0, 0, l)).node.setAttribute("default-marker-size", 0); var h = document.createElementNS(t.globals.SVGNS, "g"); h.classList.add("apexcharts-series-markers"), h.appendChild(o.node), r.appendChild(h) } } } }, { key: "enlargeCurrentPoint", value: function (t, e) { var i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : null, a = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : null, s = this.w; "bubble" !== s.config.chart.type && this.newPointSize(t, e); var r = e.getAttribute("cx"), o = e.getAttribute("cy"); if (null !== i && null !== a && (r = i, o = a), this.tooltipPosition.moveXCrosshairs(r), !this.fixedTooltip) { if ("radar" === s.config.chart.type) { var n = this.ttCtx.getElGrid().getBoundingClientRect(); r = this.ttCtx.e.clientX - n.left } this.tooltipPosition.moveTooltip(r, o, s.config.markers.hover.size) } } }, { key: "enlargePoints", value: function (t) { for (var e = this.w, i = this, a = this.ttCtx, s = t, r = e.globals.dom.baseEl.querySelectorAll(".apexcharts-series:not(.apexcharts-series-collapsed) .apexcharts-marker"), o = e.config.markers.hover.size, n = 0; n < r.length; n++) { var l = r[n].getAttribute("rel"), h = r[n].getAttribute("index"); if (void 0 === o && (o = e.globals.markers.size[h] + e.config.markers.hover.sizeOffset), s === parseInt(l, 10)) { i.newPointSize(s, r[n]); var c = r[n].getAttribute("cx"), d = r[n].getAttribute("cy"); i.tooltipPosition.moveXCrosshairs(c), a.fixedTooltip || i.tooltipPosition.moveTooltip(c, d, o) } else i.oldPointSize(r[n]) } } }, { key: "newPointSize", value: function (t, e) { var i = this.w, a = i.config.markers.hover.size, s = 0 === t ? e.parentNode.firstChild : e.parentNode.lastChild; if ("0" !== s.getAttribute("default-marker-size")) { var r = parseInt(s.getAttribute("index"), 10); void 0 === a && (a = i.globals.markers.size[r] + i.config.markers.hover.sizeOffset), a < 0 && (a = 0), s.setAttribute("r", a) } } }, { key: "oldPointSize", value: function (t) { var e = parseFloat(t.getAttribute("default-marker-size")); t.setAttribute("r", e) } }, { key: "resetPointsSize", value: function () { for (var t = this.w.globals.dom.baseEl.querySelectorAll(".apexcharts-series:not(.apexcharts-series-collapsed) .apexcharts-marker"), e = 0; e < t.length; e++) { var i = parseFloat(t[e].getAttribute("default-marker-size")); x.isNumber(i) && i >= 0 ? t[e].setAttribute("r", i) : t[e].setAttribute("r", 0) } } }]), t }(), ft = function () { function t(e) { a(this, t), this.w = e.w; var i = this.w; this.ttCtx = e, this.isVerticalGroupedRangeBar = !i.globals.isBarHorizontal && "rangeBar" === i.config.chart.type && i.config.plotOptions.bar.rangeBarGroupRows } return r(t, [{ key: "getAttr", value: function (t, e) { return parseFloat(t.target.getAttribute(e)) } }, { key: "handleHeatTreeTooltip", value: function (t) { var e = t.e, i = t.opt, a = t.x, s = t.y, r = t.type, o = this.ttCtx, n = this.w; if (e.target.classList.contains("apexcharts-".concat(r, "-rect"))) { var l = this.getAttr(e, "i"), h = this.getAttr(e, "j"), c = this.getAttr(e, "cx"), d = this.getAttr(e, "cy"), g = this.getAttr(e, "width"), u = this.getAttr(e, "height"); if (o.tooltipLabels.drawSeriesTexts({ ttItems: i.ttItems, i: l, j: h, shared: !1, e: e }), n.globals.capturedSeriesIndex = l, n.globals.capturedDataPointIndex = h, a = c + o.tooltipRect.ttWidth / 2 + g, s = d + o.tooltipRect.ttHeight / 2 - u / 2, o.tooltipPosition.moveXCrosshairs(c + g / 2), a > n.globals.gridWidth / 2 && (a = c - o.tooltipRect.ttWidth / 2 + g), o.w.config.tooltip.followCursor) { var p = n.globals.dom.elWrap.getBoundingClientRect(); a = n.globals.clientX - p.left - (a > n.globals.gridWidth / 2 ? o.tooltipRect.ttWidth : 0), s = n.globals.clientY - p.top - (s > n.globals.gridHeight / 2 ? o.tooltipRect.ttHeight : 0) } } return { x: a, y: s } } }, { key: "handleMarkerTooltip", value: function (t) { var e, i, a = t.e, s = t.opt, r = t.x, o = t.y, n = this.w, l = this.ttCtx; if (a.target.classList.contains("apexcharts-marker")) { var h = parseInt(s.paths.getAttribute("cx"), 10), c = parseInt(s.paths.getAttribute("cy"), 10), d = parseFloat(s.paths.getAttribute("val")); if (i = parseInt(s.paths.getAttribute("rel"), 10), e = parseInt(s.paths.parentNode.parentNode.parentNode.getAttribute("rel"), 10) - 1, l.intersect) { var g = x.findAncestor(s.paths, "apexcharts-series"); g && (e = parseInt(g.getAttribute("data:realIndex"), 10)) } if (l.tooltipLabels.drawSeriesTexts({ ttItems: s.ttItems, i: e, j: i, shared: !l.showOnIntersect && n.config.tooltip.shared, e: a }), "mouseup" === a.type && l.markerClick(a, e, i), n.globals.capturedSeriesIndex = e, n.globals.capturedDataPointIndex = i, r = h, o = c + n.globals.translateY - 1.4 * l.tooltipRect.ttHeight, l.w.config.tooltip.followCursor) { var u = l.getElGrid().getBoundingClientRect(); o = l.e.clientY + n.globals.translateY - u.top } d < 0 && (o = c), l.marker.enlargeCurrentPoint(i, s.paths, r, o) } return { x: r, y: o } } }, { key: "handleBarTooltip", value: function (t) { var e, i, a = t.e, s = t.opt, r = this.w, o = this.ttCtx, n = o.getElTooltip(), l = 0, h = 0, c = 0, d = this.getBarTooltipXY({ e: a, opt: s }); e = d.i; var g = d.barHeight, u = d.j; r.globals.capturedSeriesIndex = e, r.globals.capturedDataPointIndex = u, r.globals.isBarHorizontal && o.tooltipUtil.hasBars() || !r.config.tooltip.shared ? (h = d.x, c = d.y, i = Array.isArray(r.config.stroke.width) ? r.config.stroke.width[e] : r.config.stroke.width, l = h) : r.globals.comboCharts || r.config.tooltip.shared || (l /= 2), isNaN(c) && (c = r.globals.svgHeight - o.tooltipRect.ttHeight); var p = parseInt(s.paths.parentNode.getAttribute("data:realIndex"), 10), f = r.globals.isMultipleYAxis ? r.config.yaxis[p] && r.config.yaxis[p].reversed : r.config.yaxis[0].reversed; if (h + o.tooltipRect.ttWidth > r.globals.gridWidth && !f ? h -= o.tooltipRect.ttWidth : h < 0 && (h = 0), o.w.config.tooltip.followCursor) { var x = o.getElGrid().getBoundingClientRect(); c = o.e.clientY - x.top } null === o.tooltip && (o.tooltip = r.globals.dom.baseEl.querySelector(".apexcharts-tooltip")), r.config.tooltip.shared || (r.globals.comboBarCount > 0 ? o.tooltipPosition.moveXCrosshairs(l + i / 2) : o.tooltipPosition.moveXCrosshairs(l)), !o.fixedTooltip && (!r.config.tooltip.shared || r.globals.isBarHorizontal && o.tooltipUtil.hasBars()) && (f && (h -= o.tooltipRect.ttWidth) < 0 && (h = 0), !f || r.globals.isBarHorizontal && o.tooltipUtil.hasBars() || (c = c + g - 2 * (r.globals.series[e][u] < 0 ? g : 0)), c = c + r.globals.translateY - o.tooltipRect.ttHeight / 2, n.style.left = h + r.globals.translateX + "px", n.style.top = c + "px") } }, { key: "getBarTooltipXY", value: function (t) { var e = this, i = t.e, a = t.opt, s = this.w, r = null, o = this.ttCtx, n = 0, l = 0, h = 0, c = 0, d = 0, g = i.target.classList; if (g.contains("apexcharts-bar-area") || g.contains("apexcharts-candlestick-area") || g.contains("apexcharts-boxPlot-area") || g.contains("apexcharts-rangebar-area")) { var u = i.target, p = u.getBoundingClientRect(), f = a.elGrid.getBoundingClientRect(), x = p.height; d = p.height; var b = p.width, v = parseInt(u.getAttribute("cx"), 10), m = parseInt(u.getAttribute("cy"), 10); c = parseFloat(u.getAttribute("barWidth")); var y = "touchmove" === i.type ? i.touches[0].clientX : i.clientX; r = parseInt(u.getAttribute("j"), 10), n = parseInt(u.parentNode.getAttribute("rel"), 10) - 1; var w = u.getAttribute("data-range-y1"), k = u.getAttribute("data-range-y2"); s.globals.comboCharts && (n = parseInt(u.parentNode.getAttribute("data:realIndex"), 10)); var A = function (t) { return s.globals.isXNumeric ? v - b / 2 : e.isVerticalGroupedRangeBar ? v + b / 2 : v - o.dataPointsDividedWidth + b / 2 }, S = function () { return m - o.dataPointsDividedHeight + x / 2 - o.tooltipRect.ttHeight / 2 }; o.tooltipLabels.drawSeriesTexts({ ttItems: a.ttItems, i: n, j: r, y1: w ? parseInt(w, 10) : null, y2: k ? parseInt(k, 10) : null, shared: !o.showOnIntersect && s.config.tooltip.shared, e: i }), s.config.tooltip.followCursor ? s.globals.isBarHorizontal ? (l = y - f.left + 15, h = S()) : (l = A(), h = i.clientY - f.top - o.tooltipRect.ttHeight / 2 - 15) : s.globals.isBarHorizontal ? ((l = v) < o.xyRatios.baseLineInvertedY && (l = v - o.tooltipRect.ttWidth), h = S()) : (l = A(), h = m) } return { x: l, y: h, barHeight: d, barWidth: c, i: n, j: r } } }]), t }(), xt = function () { function t(e) { a(this, t), this.w = e.w, this.ttCtx = e } return r(t, [{ key: "drawXaxisTooltip", value: function () { var t = this.w, e = this.ttCtx, i = "bottom" === t.config.xaxis.position; e.xaxisOffY = i ? t.globals.gridHeight + 1 : -t.globals.xAxisHeight - t.config.xaxis.axisTicks.height + 3; var a = i ? "apexcharts-xaxistooltip apexcharts-xaxistooltip-bottom" : "apexcharts-xaxistooltip apexcharts-xaxistooltip-top", s = t.globals.dom.elWrap; e.isXAxisTooltipEnabled && (null === t.globals.dom.baseEl.querySelector(".apexcharts-xaxistooltip") && (e.xaxisTooltip = document.createElement("div"), e.xaxisTooltip.setAttribute("class", a + " apexcharts-theme-" + t.config.tooltip.theme), s.appendChild(e.xaxisTooltip), e.xaxisTooltipText = document.createElement("div"), e.xaxisTooltipText.classList.add("apexcharts-xaxistooltip-text"), e.xaxisTooltipText.style.fontFamily = t.config.xaxis.tooltip.style.fontFamily || t.config.chart.fontFamily, e.xaxisTooltipText.style.fontSize = t.config.xaxis.tooltip.style.fontSize, e.xaxisTooltip.appendChild(e.xaxisTooltipText))) } }, { key: "drawYaxisTooltip", value: function () { for (var t = this.w, e = this.ttCtx, i = 0; i < t.config.yaxis.length; i++) { var a = t.config.yaxis[i].opposite || t.config.yaxis[i].crosshairs.opposite; e.yaxisOffX = a ? t.globals.gridWidth + 1 : 1; var s = "apexcharts-yaxistooltip apexcharts-yaxistooltip-".concat(i, a ? " apexcharts-yaxistooltip-right" : " apexcharts-yaxistooltip-left"), r = t.globals.dom.elWrap; null === t.globals.dom.baseEl.querySelector(".apexcharts-yaxistooltip apexcharts-yaxistooltip-".concat(i)) && (e.yaxisTooltip = document.createElement("div"), e.yaxisTooltip.setAttribute("class", s + " apexcharts-theme-" + t.config.tooltip.theme), r.appendChild(e.yaxisTooltip), 0 === i && (e.yaxisTooltipText = []), e.yaxisTooltipText[i] = document.createElement("div"), e.yaxisTooltipText[i].classList.add("apexcharts-yaxistooltip-text"), e.yaxisTooltip.appendChild(e.yaxisTooltipText[i])) } } }, { key: "setXCrosshairWidth", value: function () { var t = this.w, e = this.ttCtx, i = e.getElXCrosshairs(); if (e.xcrosshairsWidth = parseInt(t.config.xaxis.crosshairs.width, 10), t.globals.comboCharts) { var a = t.globals.dom.baseEl.querySelector(".apexcharts-bar-area"); if (null !== a && "barWidth" === t.config.xaxis.crosshairs.width) { var s = parseFloat(a.getAttribute("barWidth")); e.xcrosshairsWidth = s } else if ("tickWidth" === t.config.xaxis.crosshairs.width) { var r = t.globals.labels.length; e.xcrosshairsWidth = t.globals.gridWidth / r } } else if ("tickWidth" === t.config.xaxis.crosshairs.width) { var o = t.globals.labels.length; e.xcrosshairsWidth = t.globals.gridWidth / o } else if ("barWidth" === t.config.xaxis.crosshairs.width) { var n = t.globals.dom.baseEl.querySelector(".apexcharts-bar-area"); if (null !== n) { var l = parseFloat(n.getAttribute("barWidth")); e.xcrosshairsWidth = l } else e.xcrosshairsWidth = 1 } t.globals.isBarHorizontal && (e.xcrosshairsWidth = 0), null !== i && e.xcrosshairsWidth > 0 && i.setAttribute("width", e.xcrosshairsWidth) } }, { key: "handleYCrosshair", value: function () { var t = this.w, e = this.ttCtx; e.ycrosshairs = t.globals.dom.baseEl.querySelector(".apexcharts-ycrosshairs"), e.ycrosshairsHidden = t.globals.dom.baseEl.querySelector(".apexcharts-ycrosshairs-hidden") } }, { key: "drawYaxisTooltipText", value: function (t, e, i) { var a = this.ttCtx, s = this.w, r = s.globals, o = r.seriesYAxisMap[t]; if (a.yaxisTooltips[t] && o.length > 0) { var n = r.yLabelFormatters[t], l = a.getElGrid().getBoundingClientRect(), h = o[0]; i.yRatio.length > 1 && function (t) { throw new TypeError('"' + t + '" is read-only') }("translationsIndex"); var c = (e - l.top) * i.yRatio[0], d = r.maxYArr[h] - r.minYArr[h], g = r.minYArr[h] + (d - c); s.config.yaxis[t].reversed && (g = r.maxYArr[h] - (d - c)), a.tooltipPosition.moveYCrosshairs(e - l.top), a.yaxisTooltipText[t].innerHTML = n(g), a.tooltipPosition.moveYAxisTooltip(t) } } }]), t }(), bt = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w; var i = this.w; this.tConfig = i.config.tooltip, this.tooltipUtil = new dt(this), this.tooltipLabels = new gt(this), this.tooltipPosition = new ut(this), this.marker = new pt(this), this.intersect = new ft(this), this.axesTooltip = new xt(this), this.showOnIntersect = this.tConfig.intersect, this.showTooltipTitle = this.tConfig.x.show, this.fixedTooltip = this.tConfig.fixed.enabled, this.xaxisTooltip = null, this.yaxisTTEls = null, this.isBarShared = !i.globals.isBarHorizontal && this.tConfig.shared, this.lastHoverTime = Date.now() } return r(t, [{ key: "getElTooltip", value: function (t) { return t || (t = this), t.w.globals.dom.baseEl ? t.w.globals.dom.baseEl.querySelector(".apexcharts-tooltip") : null } }, { key: "getElXCrosshairs", value: function () { return this.w.globals.dom.baseEl.querySelector(".apexcharts-xcrosshairs") } }, { key: "getElGrid", value: function () { return this.w.globals.dom.baseEl.querySelector(".apexcharts-grid") } }, { key: "drawTooltip", value: function (t) { var e = this.w; this.xyRatios = t, this.isXAxisTooltipEnabled = e.config.xaxis.tooltip.enabled && e.globals.axisCharts, this.yaxisTooltips = e.config.yaxis.map((function (t, i) { return !!(t.show && t.tooltip.enabled && e.globals.axisCharts) })), this.allTooltipSeriesGroups = [], e.globals.axisCharts || (this.showTooltipTitle = !1); var i = document.createElement("div"); if (i.classList.add("apexcharts-tooltip"), e.config.tooltip.cssClass && i.classList.add(e.config.tooltip.cssClass), i.classList.add("apexcharts-theme-".concat(this.tConfig.theme)), e.globals.dom.elWrap.appendChild(i), e.globals.axisCharts) { this.axesTooltip.drawXaxisTooltip(), this.axesTooltip.drawYaxisTooltip(), this.axesTooltip.setXCrosshairWidth(), this.axesTooltip.handleYCrosshair(); var a = new V(this.ctx); this.xAxisTicksPositions = a.getXAxisTicksPositions() } if (!e.globals.comboCharts && !this.tConfig.intersect && "rangeBar" !== e.config.chart.type || this.tConfig.shared || (this.showOnIntersect = !0), 0 !== e.config.markers.size && 0 !== e.globals.markers.largestSize || this.marker.drawDynamicPoints(this), e.globals.collapsedSeries.length !== e.globals.series.length) { this.dataPointsDividedHeight = e.globals.gridHeight / e.globals.dataPoints, this.dataPointsDividedWidth = e.globals.gridWidth / e.globals.dataPoints, this.showTooltipTitle && (this.tooltipTitle = document.createElement("div"), this.tooltipTitle.classList.add("apexcharts-tooltip-title"), this.tooltipTitle.style.fontFamily = this.tConfig.style.fontFamily || e.config.chart.fontFamily, this.tooltipTitle.style.fontSize = this.tConfig.style.fontSize, i.appendChild(this.tooltipTitle)); var s = e.globals.series.length; (e.globals.xyCharts || e.globals.comboCharts) && this.tConfig.shared && (s = this.showOnIntersect ? 1 : e.globals.series.length), this.legendLabels = e.globals.dom.baseEl.querySelectorAll(".apexcharts-legend-text"), this.ttItems = this.createTTElements(s), this.addSVGEvents() } } }, { key: "createTTElements", value: function (t) { for (var e = this, i = this.w, a = [], s = this.getElTooltip(), r = function (r) { var o = document.createElement("div"); o.classList.add("apexcharts-tooltip-series-group"), o.style.order = i.config.tooltip.inverseOrder ? t - r : r + 1, e.tConfig.shared && e.tConfig.enabledOnSeries && Array.isArray(e.tConfig.enabledOnSeries) && e.tConfig.enabledOnSeries.indexOf(r) < 0 && o.classList.add("apexcharts-tooltip-series-group-hidden"); var n = document.createElement("span"); n.classList.add("apexcharts-tooltip-marker"), n.style.backgroundColor = i.globals.colors[r], o.appendChild(n); var l = document.createElement("div"); l.classList.add("apexcharts-tooltip-text"), l.style.fontFamily = e.tConfig.style.fontFamily || i.config.chart.fontFamily, l.style.fontSize = e.tConfig.style.fontSize, ["y", "goals", "z"].forEach((function (t) { var e = document.createElement("div"); e.classList.add("apexcharts-tooltip-".concat(t, "-group")); var i = document.createElement("span"); i.classList.add("apexcharts-tooltip-text-".concat(t, "-label")), e.appendChild(i); var a = document.createElement("span"); a.classList.add("apexcharts-tooltip-text-".concat(t, "-value")), e.appendChild(a), l.appendChild(e) })), o.appendChild(l), s.appendChild(o), a.push(o) }, o = 0; o < t; o++)r(o); return a } }, { key: "addSVGEvents", value: function () { var t = this.w, e = t.config.chart.type, i = this.getElTooltip(), a = !("bar" !== e && "candlestick" !== e && "boxPlot" !== e && "rangeBar" !== e), s = "area" === e || "line" === e || "scatter" === e || "bubble" === e || "radar" === e, r = t.globals.dom.Paper.node, o = this.getElGrid(); o && (this.seriesBound = o.getBoundingClientRect()); var n, l = [], h = [], c = { hoverArea: r, elGrid: o, tooltipEl: i, tooltipY: l, tooltipX: h, ttItems: this.ttItems }; if (t.globals.axisCharts && (s ? n = t.globals.dom.baseEl.querySelectorAll(".apexcharts-series[data\\:longestSeries='true'] .apexcharts-marker") : a ? n = t.globals.dom.baseEl.querySelectorAll(".apexcharts-series .apexcharts-bar-area, .apexcharts-series .apexcharts-candlestick-area, .apexcharts-series .apexcharts-boxPlot-area, .apexcharts-series .apexcharts-rangebar-area") : "heatmap" !== e && "treemap" !== e || (n = t.globals.dom.baseEl.querySelectorAll(".apexcharts-series .apexcharts-heatmap, .apexcharts-series .apexcharts-treemap")), n && n.length)) for (var d = 0; d < n.length; d++)l.push(n[d].getAttribute("cy")), h.push(n[d].getAttribute("cx")); if (t.globals.xyCharts && !this.showOnIntersect || t.globals.comboCharts && !this.showOnIntersect || a && this.tooltipUtil.hasBars() && this.tConfig.shared) this.addPathsEventListeners([r], c); else if (a && !t.globals.comboCharts || s && this.showOnIntersect) this.addDatapointEventsListeners(c); else if (!t.globals.axisCharts || "heatmap" === e || "treemap" === e) { var g = t.globals.dom.baseEl.querySelectorAll(".apexcharts-series"); this.addPathsEventListeners(g, c) } if (this.showOnIntersect) { var u = t.globals.dom.baseEl.querySelectorAll(".apexcharts-line-series .apexcharts-marker, .apexcharts-area-series .apexcharts-marker"); u.length > 0 && this.addPathsEventListeners(u, c), this.tooltipUtil.hasBars() && !this.tConfig.shared && this.addDatapointEventsListeners(c) } } }, { key: "drawFixedTooltipRect", value: function () { var t = this.w, e = this.getElTooltip(), i = e.getBoundingClientRect(), a = i.width + 10, s = i.height + 10, r = this.tConfig.fixed.offsetX, o = this.tConfig.fixed.offsetY, n = this.tConfig.fixed.position.toLowerCase(); return n.indexOf("right") > -1 && (r = r + t.globals.svgWidth - a + 10), n.indexOf("bottom") > -1 && (o = o + t.globals.svgHeight - s - 10), e.style.left = r + "px", e.style.top = o + "px", { x: r, y: o, ttWidth: a, ttHeight: s } } }, { key: "addDatapointEventsListeners", value: function (t) { var e = this.w.globals.dom.baseEl.querySelectorAll(".apexcharts-series-markers .apexcharts-marker, .apexcharts-bar-area, .apexcharts-candlestick-area, .apexcharts-boxPlot-area, .apexcharts-rangebar-area"); this.addPathsEventListeners(e, t) } }, { key: "addPathsEventListeners", value: function (t, e) { for (var i = this, a = function (a) { var s = { paths: t[a], tooltipEl: e.tooltipEl, tooltipY: e.tooltipY, tooltipX: e.tooltipX, elGrid: e.elGrid, hoverArea: e.hoverArea, ttItems: e.ttItems };["mousemove", "mouseup", "touchmove", "mouseout", "touchend"].map((function (e) { return t[a].addEventListener(e, i.onSeriesHover.bind(i, s), { capture: !1, passive: !0 }) })) }, s = 0; s < t.length; s++)a(s) } }, { key: "onSeriesHover", value: function (t, e) { var i = this, a = Date.now() - this.lastHoverTime; a >= 100 ? this.seriesHover(t, e) : (clearTimeout(this.seriesHoverTimeout), this.seriesHoverTimeout = setTimeout((function () { i.seriesHover(t, e) }), 100 - a)) } }, { key: "seriesHover", value: function (t, e) { var i = this; this.lastHoverTime = Date.now(); var a = [], s = this.w; s.config.chart.group && (a = this.ctx.getGroupedCharts()), s.globals.axisCharts && (s.globals.minX === -1 / 0 && s.globals.maxX === 1 / 0 || 0 === s.globals.dataPoints) || (a.length ? a.forEach((function (a) { var s = i.getElTooltip(a), r = { paths: t.paths, tooltipEl: s, tooltipY: t.tooltipY, tooltipX: t.tooltipX, elGrid: t.elGrid, hoverArea: t.hoverArea, ttItems: a.w.globals.tooltip.ttItems }; a.w.globals.minX === i.w.globals.minX && a.w.globals.maxX === i.w.globals.maxX && a.w.globals.tooltip.seriesHoverByContext({ chartCtx: a, ttCtx: a.w.globals.tooltip, opt: r, e: e }) })) : this.seriesHoverByContext({ chartCtx: this.ctx, ttCtx: this.w.globals.tooltip, opt: t, e: e })) } }, { key: "seriesHoverByContext", value: function (t) { var e = t.chartCtx, i = t.ttCtx, a = t.opt, s = t.e, r = e.w, o = this.getElTooltip(); if (o) { if (i.tooltipRect = { x: 0, y: 0, ttWidth: o.getBoundingClientRect().width, ttHeight: o.getBoundingClientRect().height }, i.e = s, i.tooltipUtil.hasBars() && !r.globals.comboCharts && !i.isBarShared) if (this.tConfig.onDatasetHover.highlightDataSeries) new W(e).toggleSeriesOnHover(s, s.target.parentNode); i.fixedTooltip && i.drawFixedTooltipRect(), r.globals.axisCharts ? i.axisChartsTooltips({ e: s, opt: a, tooltipRect: i.tooltipRect }) : i.nonAxisChartsTooltips({ e: s, opt: a, tooltipRect: i.tooltipRect }) } } }, { key: "axisChartsTooltips", value: function (t) { var e, i, a = t.e, s = t.opt, r = this.w, o = s.elGrid.getBoundingClientRect(), n = "touchmove" === a.type ? a.touches[0].clientX : a.clientX, l = "touchmove" === a.type ? a.touches[0].clientY : a.clientY; if (this.clientY = l, this.clientX = n, r.globals.capturedSeriesIndex = -1, r.globals.capturedDataPointIndex = -1, l < o.top || l > o.top + o.height) this.handleMouseOut(s); else { if (Array.isArray(this.tConfig.enabledOnSeries) && !r.config.tooltip.shared) { var h = parseInt(s.paths.getAttribute("index"), 10); if (this.tConfig.enabledOnSeries.indexOf(h) < 0) return void this.handleMouseOut(s) } var c = this.getElTooltip(), d = this.getElXCrosshairs(), g = r.globals.xyCharts || "bar" === r.config.chart.type && !r.globals.isBarHorizontal && this.tooltipUtil.hasBars() && this.tConfig.shared || r.globals.comboCharts && this.tooltipUtil.hasBars(); if ("mousemove" === a.type || "touchmove" === a.type || "mouseup" === a.type) { if (r.globals.collapsedSeries.length + r.globals.ancillaryCollapsedSeries.length === r.globals.series.length) return; null !== d && d.classList.add("apexcharts-active"); var u = this.yaxisTooltips.filter((function (t) { return !0 === t })); if (null !== this.ycrosshairs && u.length && this.ycrosshairs.classList.add("apexcharts-active"), g && !this.showOnIntersect) this.handleStickyTooltip(a, n, l, s); else if ("heatmap" === r.config.chart.type || "treemap" === r.config.chart.type) { var p = this.intersect.handleHeatTreeTooltip({ e: a, opt: s, x: e, y: i, type: r.config.chart.type }); e = p.x, i = p.y, c.style.left = e + "px", c.style.top = i + "px" } else this.tooltipUtil.hasBars() && this.intersect.handleBarTooltip({ e: a, opt: s }), this.tooltipUtil.hasMarkers() && this.intersect.handleMarkerTooltip({ e: a, opt: s, x: e, y: i }); if (this.yaxisTooltips.length) for (var f = 0; f < r.config.yaxis.length; f++)this.axesTooltip.drawYaxisTooltipText(f, l, this.xyRatios); s.tooltipEl.classList.add("apexcharts-active") } else "mouseout" !== a.type && "touchend" !== a.type || this.handleMouseOut(s) } } }, { key: "nonAxisChartsTooltips", value: function (t) { var e = t.e, i = t.opt, a = t.tooltipRect, s = this.w, r = i.paths.getAttribute("rel"), o = this.getElTooltip(), n = s.globals.dom.elWrap.getBoundingClientRect(); if ("mousemove" === e.type || "touchmove" === e.type) { o.classList.add("apexcharts-active"), this.tooltipLabels.drawSeriesTexts({ ttItems: i.ttItems, i: parseInt(r, 10) - 1, shared: !1 }); var l = s.globals.clientX - n.left - a.ttWidth / 2, h = s.globals.clientY - n.top - a.ttHeight - 10; if (o.style.left = l + "px", o.style.top = h + "px", s.config.legend.tooltipHoverFormatter) { var c = r - 1, d = (0, s.config.legend.tooltipHoverFormatter)(this.legendLabels[c].getAttribute("data:default-text"), { seriesIndex: c, dataPointIndex: c, w: s }); this.legendLabels[c].innerHTML = d } } else "mouseout" !== e.type && "touchend" !== e.type || (o.classList.remove("apexcharts-active"), s.config.legend.tooltipHoverFormatter && this.legendLabels.forEach((function (t) { var e = t.getAttribute("data:default-text"); t.innerHTML = decodeURIComponent(e) }))) } }, { key: "handleStickyTooltip", value: function (t, e, i, a) { var s = this.w, r = this.tooltipUtil.getNearestValues({ context: this, hoverArea: a.hoverArea, elGrid: a.elGrid, clientX: e, clientY: i }), o = r.j, n = r.capturedSeries; s.globals.collapsedSeriesIndices.includes(n) && (n = null); var l = a.elGrid.getBoundingClientRect(); if (r.hoverX < 0 || r.hoverX > l.width) this.handleMouseOut(a); else if (null !== n) this.handleStickyCapturedSeries(t, n, a, o); else if (this.tooltipUtil.isXoverlap(o) || s.globals.isBarHorizontal) { var h = s.globals.series.findIndex((function (t, e) { return !s.globals.collapsedSeriesIndices.includes(e) })); this.create(t, this, h, o, a.ttItems) } } }, { key: "handleStickyCapturedSeries", value: function (t, e, i, a) { var s = this.w; if (!this.tConfig.shared && null === s.globals.series[e][a]) return void this.handleMouseOut(i); if (void 0 !== s.globals.series[e][a]) this.tConfig.shared && this.tooltipUtil.isXoverlap(a) && this.tooltipUtil.isInitialSeriesSameLen() ? this.create(t, this, e, a, i.ttItems) : this.create(t, this, e, a, i.ttItems, !1); else if (this.tooltipUtil.isXoverlap(a)) { var r = s.globals.series.findIndex((function (t, e) { return !s.globals.collapsedSeriesIndices.includes(e) })); this.create(t, this, r, a, i.ttItems) } } }, { key: "deactivateHoverFilter", value: function () { for (var t = this.w, e = new m(this.ctx), i = t.globals.dom.Paper.select(".apexcharts-bar-area"), a = 0; a < i.length; a++)e.pathMouseLeave(i[a]) } }, { key: "handleMouseOut", value: function (t) { var e = this.w, i = this.getElXCrosshairs(); if (t.tooltipEl.classList.remove("apexcharts-active"), this.deactivateHoverFilter(), "bubble" !== e.config.chart.type && this.marker.resetPointsSize(), null !== i && i.classList.remove("apexcharts-active"), null !== this.ycrosshairs && this.ycrosshairs.classList.remove("apexcharts-active"), this.isXAxisTooltipEnabled && this.xaxisTooltip.classList.remove("apexcharts-active"), this.yaxisTooltips.length) { null === this.yaxisTTEls && (this.yaxisTTEls = e.globals.dom.baseEl.querySelectorAll(".apexcharts-yaxistooltip")); for (var a = 0; a < this.yaxisTTEls.length; a++)this.yaxisTTEls[a].classList.remove("apexcharts-active") } e.config.legend.tooltipHoverFormatter && this.legendLabels.forEach((function (t) { var e = t.getAttribute("data:default-text"); t.innerHTML = decodeURIComponent(e) })) } }, { key: "markerClick", value: function (t, e, i) { var a = this.w; "function" == typeof a.config.chart.events.markerClick && a.config.chart.events.markerClick(t, this.ctx, { seriesIndex: e, dataPointIndex: i, w: a }), this.ctx.events.fireEvent("markerClick", [t, this.ctx, { seriesIndex: e, dataPointIndex: i, w: a }]) } }, { key: "create", value: function (t, i, a, s, r) { var o, n, l, h, c, d, g, u, p, f, x, b, v, y, w, k, A = arguments.length > 5 && void 0 !== arguments[5] ? arguments[5] : null, S = this.w, C = i; "mouseup" === t.type && this.markerClick(t, a, s), null === A && (A = this.tConfig.shared); var L = this.tooltipUtil.hasMarkers(a), P = this.tooltipUtil.getElBars(); if (S.config.legend.tooltipHoverFormatter) { var M = S.config.legend.tooltipHoverFormatter, I = Array.from(this.legendLabels); I.forEach((function (t) { var e = t.getAttribute("data:default-text"); t.innerHTML = decodeURIComponent(e) })); for (var T = 0; T < I.length; T++) { var z = I[T], X = parseInt(z.getAttribute("i"), 10), E = decodeURIComponent(z.getAttribute("data:default-text")), Y = M(E, { seriesIndex: A ? X : a, dataPointIndex: s, w: S }); if (A) z.innerHTML = S.globals.collapsedSeriesIndices.indexOf(X) < 0 ? Y : E; else if (z.innerHTML = X === a ? Y : E, a === X) break } } var F = e(e({ ttItems: r, i: a, j: s }, void 0 !== (null === (o = S.globals.seriesRange) || void 0 === o || null === (n = o[a]) || void 0 === n || null === (l = n[s]) || void 0 === l || null === (h = l.y[0]) || void 0 === h ? void 0 : h.y1) && { y1: null === (c = S.globals.seriesRange) || void 0 === c || null === (d = c[a]) || void 0 === d || null === (g = d[s]) || void 0 === g || null === (u = g.y[0]) || void 0 === u ? void 0 : u.y1 }), void 0 !== (null === (p = S.globals.seriesRange) || void 0 === p || null === (f = p[a]) || void 0 === f || null === (x = f[s]) || void 0 === x || null === (b = x.y[0]) || void 0 === b ? void 0 : b.y2) && { y2: null === (v = S.globals.seriesRange) || void 0 === v || null === (y = v[a]) || void 0 === y || null === (w = y[s]) || void 0 === w || null === (k = w.y[0]) || void 0 === k ? void 0 : k.y2 }); if (A) { if (C.tooltipLabels.drawSeriesTexts(e(e({}, F), {}, { shared: !this.showOnIntersect && this.tConfig.shared })), L) S.globals.markers.largestSize > 0 ? C.marker.enlargePoints(s) : C.tooltipPosition.moveDynamicPointsOnHover(s); else if (this.tooltipUtil.hasBars() && (this.barSeriesHeight = this.tooltipUtil.getBarsHeight(P), this.barSeriesHeight > 0)) { var R = new m(this.ctx), H = S.globals.dom.Paper.select(".apexcharts-bar-area[j='".concat(s, "']")); this.deactivateHoverFilter(), this.tooltipPosition.moveStickyTooltipOverBars(s, a); for (var D = 0; D < H.length; D++)R.pathMouseEnter(H[D]) } } else C.tooltipLabels.drawSeriesTexts(e({ shared: !1 }, F)), this.tooltipUtil.hasBars() && C.tooltipPosition.moveStickyTooltipOverBars(s, a), L && C.tooltipPosition.moveMarkers(a, s) } }]), t }(), vt = function () { function t(e) { a(this, t), this.w = e.w, this.barCtx = e, this.totalFormatter = this.w.config.plotOptions.bar.dataLabels.total.formatter, this.totalFormatter || (this.totalFormatter = this.w.config.dataLabels.formatter) } return r(t, [{ key: "handleBarDataLabels", value: function (t) { var e, i, a = t.x, s = t.y, r = t.y1, o = t.y2, n = t.i, l = t.j, h = t.realIndex, c = t.columnGroupIndex, d = t.series, g = t.barHeight, u = t.barWidth, p = t.barXPosition, f = t.barYPosition, x = t.visibleSeries, b = t.renderedPath, v = this.w, y = new m(this.barCtx.ctx), w = Array.isArray(this.barCtx.strokeWidth) ? this.barCtx.strokeWidth[h] : this.barCtx.strokeWidth; v.globals.isXNumeric && !v.globals.isBarHorizontal ? (e = a + parseFloat(u * (x + 1)), i = s + parseFloat(g * (x + 1)) - w) : (e = a + parseFloat(u * x), i = s + parseFloat(g * x)); var k, A = null, S = a, C = s, L = {}, P = v.config.dataLabels, M = this.barCtx.barOptions.dataLabels, I = this.barCtx.barOptions.dataLabels.total; void 0 !== f && this.barCtx.isRangeBar && (i = f, C = f), void 0 !== p && this.barCtx.isVerticalGroupedRangeBar && (e = p, S = p); var T = P.offsetX, z = P.offsetY, X = { width: 0, height: 0 }; if (v.config.dataLabels.enabled) { var E = this.barCtx.series[n][l]; X = y.getTextRects(v.globals.yLabelFormatters[0](E), parseFloat(P.style.fontSize)) } var Y = { x: a, y: s, i: n, j: l, realIndex: h, columnGroupIndex: c, renderedPath: b, bcx: e, bcy: i, barHeight: g, barWidth: u, textRects: X, strokeWidth: w, dataLabelsX: S, dataLabelsY: C, dataLabelsConfig: P, barDataLabelsConfig: M, barTotalDataLabelsConfig: I, offX: T, offY: z }; return L = this.barCtx.isHorizontal ? this.calculateBarsDataLabelsPosition(Y) : this.calculateColumnsDataLabelsPosition(Y), b.attr({ cy: L.bcy, cx: L.bcx, j: l, val: d[n][l], barHeight: g, barWidth: u }), k = this.drawCalculatedDataLabels({ x: L.dataLabelsX, y: L.dataLabelsY, val: this.barCtx.isRangeBar ? [r, o] : d[n][l], i: h, j: l, barWidth: u, barHeight: g, textRects: X, dataLabelsConfig: P }), v.config.chart.stacked && I.enabled && (A = this.drawTotalDataLabels({ x: L.totalDataLabelsX, y: L.totalDataLabelsY, barWidth: u, barHeight: g, realIndex: h, textAnchor: L.totalDataLabelsAnchor, val: this.getStackedTotalDataLabel({ realIndex: h, j: l }), dataLabelsConfig: P, barTotalDataLabelsConfig: I })), { dataLabels: k, totalDataLabels: A } } }, { key: "getStackedTotalDataLabel", value: function (t) { var i = t.realIndex, a = t.j, s = this.w, r = this.barCtx.stackedSeriesTotals[a]; return this.totalFormatter && (r = this.totalFormatter(r, e(e({}, s), {}, { seriesIndex: i, dataPointIndex: a, w: s }))), r } }, { key: "calculateColumnsDataLabelsPosition", value: function (t) { var e, i, a = this.w, s = t.i, r = t.j, o = t.realIndex, n = t.columnGroupIndex, l = t.y, h = t.bcx, c = t.barWidth, d = t.barHeight, g = t.textRects, u = t.dataLabelsX, p = t.dataLabelsY, f = t.dataLabelsConfig, x = t.barDataLabelsConfig, b = t.barTotalDataLabelsConfig, v = t.strokeWidth, y = t.offX, w = t.offY, k = h; d = Math.abs(d); var A = "vertical" === a.config.plotOptions.bar.dataLabels.orientation, S = this.barCtx.barHelpers.getZeroValueEncounters({ i: s, j: r }).zeroEncounters; h = h - v / 2 + n * c; var C = a.globals.gridWidth / a.globals.dataPoints; if (this.barCtx.isVerticalGroupedRangeBar ? u += c / 2 : (u = a.globals.isXNumeric ? h - c / 2 + y : h - C + c / 2 + y, S > 0 && a.config.plotOptions.bar.hideZeroBarsWhenGrouped && (u -= c * S)), A) { u = u + g.height / 2 - v / 2 - 2 } var L = this.barCtx.series[s][r] < 0, P = l; switch (this.barCtx.isReversed && (P = l + (L ? d : -d), l -= d), x.position) { case "center": p = A ? L ? P - d / 2 + w : P + d / 2 - w : L ? P - d / 2 + g.height / 2 + w : P + d / 2 + g.height / 2 - w; break; case "bottom": p = A ? L ? P - d + w : P + d - w : L ? P - d + g.height + v + w : P + d - g.height / 2 + v - w; break; case "top": p = A ? L ? P + w : P - w : L ? P - g.height / 2 - w : P + g.height + w }if (this.barCtx.lastActiveBarSerieIndex === o && b.enabled) { var M = new m(this.barCtx.ctx).getTextRects(this.getStackedTotalDataLabel({ realIndex: o, j: r }), f.fontSize); e = L ? P - M.height / 2 - w - b.offsetY + 18 : P + M.height + w + b.offsetY - 18, i = k + (a.globals.isXNumeric ? c * (a.globals.barGroups.length - 1) - c / 2 : -(c * a.globals.barGroups.length - c / 2 - 2 * v)) + b.offsetX } return a.config.chart.stacked || (p < 0 ? p = 0 + v : p + g.height / 3 > a.globals.gridHeight && (p = a.globals.gridHeight - v)), { bcx: h, bcy: l, dataLabelsX: u, dataLabelsY: p, totalDataLabelsX: i, totalDataLabelsY: e, totalDataLabelsAnchor: "middle" } } }, { key: "calculateBarsDataLabelsPosition", value: function (t) { var e = this.w, i = t.x, a = t.i, s = t.j, r = t.realIndex, o = t.columnGroupIndex, n = t.bcy, l = t.barHeight, h = t.barWidth, c = t.textRects, d = t.dataLabelsX, g = t.strokeWidth, u = t.dataLabelsConfig, p = t.barDataLabelsConfig, f = t.barTotalDataLabelsConfig, x = t.offX, b = t.offY, v = e.globals.gridHeight / e.globals.dataPoints; h = Math.abs(h); var y, w, k = (n += o * l) - (this.barCtx.isRangeBar ? 0 : v) + l / 2 + c.height / 2 + b - 3, A = "start", S = this.barCtx.series[a][s] < 0, C = i; switch (this.barCtx.isReversed && (C = i + (S ? -h : h), i = e.globals.gridWidth - h, A = S ? "start" : "end"), p.position) { case "center": d = S ? C + h / 2 - x : Math.max(c.width / 2, C - h / 2) + x; break; case "bottom": d = S ? C + h - g - Math.round(c.width / 2) - x : C - h + g + Math.round(c.width / 2) + x; break; case "top": d = S ? C - g + Math.round(c.width / 2) - x : C - g - Math.round(c.width / 2) + x }if (this.barCtx.lastActiveBarSerieIndex === r && f.enabled) { var L = new m(this.barCtx.ctx).getTextRects(this.getStackedTotalDataLabel({ realIndex: r, j: s }), u.fontSize); S ? (y = C - g - x - f.offsetX, A = "end") : y = C + x + f.offsetX + (this.barCtx.isReversed ? -(h + g) : g), w = k - c.height / 2 + L.height / 2 + f.offsetY + g } return e.config.chart.stacked || (d < 0 ? d = d + c.width + g : d + c.width / 2 > e.globals.gridWidth && (d = e.globals.gridWidth - c.width - g)), { bcx: i, bcy: n, dataLabelsX: d, dataLabelsY: k, totalDataLabelsX: y, totalDataLabelsY: w, totalDataLabelsAnchor: A } } }, { key: "drawCalculatedDataLabels", value: function (t) { var i = t.x, a = t.y, s = t.val, r = t.i, o = t.j, n = t.textRects, l = t.barHeight, h = t.barWidth, c = t.dataLabelsConfig, d = this.w, g = "rotate(0)"; "vertical" === d.config.plotOptions.bar.dataLabels.orientation && (g = "rotate(-90, ".concat(i, ", ").concat(a, ")")); var u = new N(this.barCtx.ctx), p = new m(this.barCtx.ctx), f = c.formatter, x = null, b = d.globals.collapsedSeriesIndices.indexOf(r) > -1; if (c.enabled && !b) { x = p.group({ class: "apexcharts-data-labels", transform: g }); var v = ""; void 0 !== s && (v = f(s, e(e({}, d), {}, { seriesIndex: r, dataPointIndex: o, w: d }))), !s && d.config.plotOptions.bar.hideZeroBarsWhenGrouped && (v = ""); var y = d.globals.series[r][o] < 0, w = d.config.plotOptions.bar.dataLabels.position; if ("vertical" === d.config.plotOptions.bar.dataLabels.orientation && ("top" === w && (c.textAnchor = y ? "end" : "start"), "center" === w && (c.textAnchor = "middle"), "bottom" === w && (c.textAnchor = y ? "end" : "start")), this.barCtx.isRangeBar && this.barCtx.barOptions.dataLabels.hideOverflowingLabels) h < p.getTextRects(v, parseFloat(c.style.fontSize)).width && (v = ""); d.config.chart.stacked && this.barCtx.barOptions.dataLabels.hideOverflowingLabels && (this.barCtx.isHorizontal ? n.width / 1.6 > Math.abs(h) && (v = "") : n.height / 1.6 > Math.abs(l) && (v = "")); var k = e({}, c); this.barCtx.isHorizontal && s < 0 && ("start" === c.textAnchor ? k.textAnchor = "end" : "end" === c.textAnchor && (k.textAnchor = "start")), u.plotDataLabelsText({ x: i, y: a, text: v, i: r, j: o, parent: x, dataLabelsConfig: k, alwaysDrawDataLabel: !0, offsetCorrection: !0 }) } return x } }, { key: "drawTotalDataLabels", value: function (t) { var e, i = t.x, a = t.y, s = t.val, r = t.barWidth, o = t.barHeight, n = t.realIndex, l = t.textAnchor, h = t.barTotalDataLabelsConfig, c = this.w, d = new m(this.barCtx.ctx); return h.enabled && void 0 !== i && void 0 !== a && this.barCtx.lastActiveBarSerieIndex === n && (e = d.drawText({ x: i - (!c.globals.isBarHorizontal && c.globals.barGroups.length ? r * (c.globals.barGroups.length - 1) / 2 : 0), y: a - (c.globals.isBarHorizontal && c.globals.barGroups.length ? o * (c.globals.barGroups.length - 1) / 2 : 0), foreColor: h.style.color, text: s, textAnchor: l, fontFamily: h.style.fontFamily, fontSize: h.style.fontSize, fontWeight: h.style.fontWeight })), e } }]), t }(), mt = function () { function t(e) { a(this, t), this.w = e.w, this.barCtx = e } return r(t, [{ key: "initVariables", value: function (t) { var e = this.w; this.barCtx.series = t, this.barCtx.totalItems = 0, this.barCtx.seriesLen = 0, this.barCtx.visibleI = -1, this.barCtx.visibleItems = 1; for (var i = 0; i < t.length; i++)if (t[i].length > 0 && (this.barCtx.seriesLen = this.barCtx.seriesLen + 1, this.barCtx.totalItems += t[i].length), e.globals.isXNumeric) for (var a = 0; a < t[i].length; a++)e.globals.seriesX[i][a] > e.globals.minX && e.globals.seriesX[i][a] < e.globals.maxX && this.barCtx.visibleItems++; else this.barCtx.visibleItems = e.globals.dataPoints; 0 === this.barCtx.seriesLen && (this.barCtx.seriesLen = 1), this.barCtx.zeroSerieses = [], e.globals.comboCharts || this.checkZeroSeries({ series: t }) } }, { key: "initialPositions", value: function () { var t, e, i, a, s, r, o, n, l = this.w, h = l.globals.dataPoints; this.barCtx.isRangeBar && (h = l.globals.labels.length); var c = this.barCtx.seriesLen; if (l.config.plotOptions.bar.rangeBarGroupRows && (c = 1), this.barCtx.isHorizontal) s = (i = l.globals.gridHeight / h) / c, l.globals.isXNumeric && (s = (i = l.globals.gridHeight / this.barCtx.totalItems) / this.barCtx.seriesLen), s = s * parseInt(this.barCtx.barOptions.barHeight, 10) / 100, -1 === String(this.barCtx.barOptions.barHeight).indexOf("%") && (s = parseInt(this.barCtx.barOptions.barHeight, 10)), n = this.barCtx.baseLineInvertedY + l.globals.padHorizontal + (this.barCtx.isReversed ? l.globals.gridWidth : 0) - (this.barCtx.isReversed ? 2 * this.barCtx.baseLineInvertedY : 0), this.barCtx.isFunnel && (n = l.globals.gridWidth / 2), e = (i - s * this.barCtx.seriesLen) / 2; else { if (a = l.globals.gridWidth / this.barCtx.visibleItems, l.config.xaxis.convertedCatToNumeric && (a = l.globals.gridWidth / l.globals.dataPoints), r = a / c * parseInt(this.barCtx.barOptions.columnWidth, 10) / 100, l.globals.isXNumeric) { var d = this.barCtx.xRatio; l.globals.minXDiff && .5 !== l.globals.minXDiff && l.globals.minXDiff / d > 0 && (a = l.globals.minXDiff / d), (r = a / c * parseInt(this.barCtx.barOptions.columnWidth, 10) / 100) < 1 && (r = 1) } -1 === String(this.barCtx.barOptions.columnWidth).indexOf("%") && (r = parseInt(this.barCtx.barOptions.columnWidth, 10)), o = l.globals.gridHeight - this.barCtx.baseLineY[this.barCtx.translationsIndex] - (this.barCtx.isReversed ? l.globals.gridHeight : 0) + (this.barCtx.isReversed ? 2 * this.barCtx.baseLineY[this.barCtx.translationsIndex] : 0), t = l.globals.padHorizontal + (a - r * this.barCtx.seriesLen) / 2 } return l.globals.barHeight = s, l.globals.barWidth = r, { x: t, y: e, yDivision: i, xDivision: a, barHeight: s, barWidth: r, zeroH: o, zeroW: n } } }, { key: "initializeStackedPrevVars", value: function (t) { t.w.globals.seriesGroups.forEach((function (e) { t[e] || (t[e] = {}), t[e].prevY = [], t[e].prevX = [], t[e].prevYF = [], t[e].prevXF = [], t[e].prevYVal = [], t[e].prevXVal = [] })) } }, { key: "initializeStackedXYVars", value: function (t) { t.w.globals.seriesGroups.forEach((function (e) { t[e] || (t[e] = {}), t[e].xArrj = [], t[e].xArrjF = [], t[e].xArrjVal = [], t[e].yArrj = [], t[e].yArrjF = [], t[e].yArrjVal = [] })) } }, { key: "getPathFillColor", value: function (t, e, i, a) { var s, r, o, n, l = this.w, h = new H(this.barCtx.ctx), c = null, d = this.barCtx.barOptions.distributed ? i : e; this.barCtx.barOptions.colors.ranges.length > 0 && this.barCtx.barOptions.colors.ranges.map((function (a) { t[e][i] >= a.from && t[e][i] <= a.to && (c = a.color) })); return l.config.series[e].data[i] && l.config.series[e].data[i].fillColor && (c = l.config.series[e].data[i].fillColor), h.fillPath({ seriesNumber: this.barCtx.barOptions.distributed ? d : a, dataPointIndex: i, color: c, value: t[e][i], fillConfig: null === (s = l.config.series[e].data[i]) || void 0 === s ? void 0 : s.fill, fillType: null !== (r = l.config.series[e].data[i]) && void 0 !== r && null !== (o = r.fill) && void 0 !== o && o.type ? null === (n = l.config.series[e].data[i]) || void 0 === n ? void 0 : n.fill.type : Array.isArray(l.config.fill.type) ? l.config.fill.type[e] : l.config.fill.type }) } }, { key: "getStrokeWidth", value: function (t, e, i) { var a = 0, s = this.w; return void 0 === this.barCtx.series[t][e] || null === this.barCtx.series[t][e] ? this.barCtx.isNullValue = !0 : this.barCtx.isNullValue = !1, s.config.stroke.show && (this.barCtx.isNullValue || (a = Array.isArray(this.barCtx.strokeWidth) ? this.barCtx.strokeWidth[i] : this.barCtx.strokeWidth)), a } }, { key: "shouldApplyRadius", value: function (t) { var e = this.w, i = !1; return e.config.plotOptions.bar.borderRadius > 0 && (e.config.chart.stacked && "last" === e.config.plotOptions.bar.borderRadiusWhenStacked ? this.barCtx.lastActiveBarSerieIndex === t && (i = !0) : i = !0), i } }, { key: "barBackground", value: function (t) { var e = t.j, i = t.i, a = t.x1, s = t.x2, r = t.y1, o = t.y2, n = t.elSeries, l = this.w, h = new m(this.barCtx.ctx), c = new W(this.barCtx.ctx).getActiveConfigSeriesIndex(); if (this.barCtx.barOptions.colors.backgroundBarColors.length > 0 && c === i) { e >= this.barCtx.barOptions.colors.backgroundBarColors.length && (e %= this.barCtx.barOptions.colors.backgroundBarColors.length); var d = this.barCtx.barOptions.colors.backgroundBarColors[e], g = h.drawRect(void 0 !== a ? a : 0, void 0 !== r ? r : 0, void 0 !== s ? s : l.globals.gridWidth, void 0 !== o ? o : l.globals.gridHeight, this.barCtx.barOptions.colors.backgroundBarRadius, d, this.barCtx.barOptions.colors.backgroundBarOpacity); n.add(g), g.node.classList.add("apexcharts-backgroundBar") } } }, { key: "getColumnPaths", value: function (t) { var e, i = t.barWidth, a = t.barXPosition, s = t.y1, r = t.y2, o = t.strokeWidth, n = t.seriesGroup, l = t.realIndex, h = t.i, c = t.j, d = t.w, g = new m(this.barCtx.ctx); (o = Array.isArray(o) ? o[l] : o) || (o = 0); var u = i, p = a; null !== (e = d.config.series[l].data[c]) && void 0 !== e && e.columnWidthOffset && (p = a - d.config.series[l].data[c].columnWidthOffset / 2, u = i + d.config.series[l].data[c].columnWidthOffset); var f = o / 2, x = p + f, b = p + u - f; s += .001 - f, r += .001 + f; var v = g.move(x, s), y = g.move(x, s), w = g.line(b, s); if (d.globals.previousPaths.length > 0 && (y = this.barCtx.getPreviousPath(l, c, !1)), v = v + g.line(x, r) + g.line(b, r) + g.line(b, s) + ("around" === d.config.plotOptions.bar.borderRadiusApplication ? " Z" : " z"), y = y + g.line(x, s) + w + w + w + w + w + g.line(x, s) + ("around" === d.config.plotOptions.bar.borderRadiusApplication ? " Z" : " z"), this.shouldApplyRadius(l) && (v = g.roundPathCorners(v, d.config.plotOptions.bar.borderRadius)), d.config.chart.stacked) { var k = this.barCtx; (k = this.barCtx[n]).yArrj.push(r - f), k.yArrjF.push(Math.abs(s - r + o)), k.yArrjVal.push(this.barCtx.series[h][c]) } return { pathTo: v, pathFrom: y } } }, { key: "getBarpaths", value: function (t) { var e, i = t.barYPosition, a = t.barHeight, s = t.x1, r = t.x2, o = t.strokeWidth, n = t.seriesGroup, l = t.realIndex, h = t.i, c = t.j, d = t.w, g = new m(this.barCtx.ctx); (o = Array.isArray(o) ? o[l] : o) || (o = 0); var u = i, p = a; null !== (e = d.config.series[l].data[c]) && void 0 !== e && e.barHeightOffset && (u = i - d.config.series[l].data[c].barHeightOffset / 2, p = a + d.config.series[l].data[c].barHeightOffset); var f = o / 2, x = u + f, b = u + p - f; s += .001 - f, r += .001 + f; var v = g.move(s, x), y = g.move(s, x); d.globals.previousPaths.length > 0 && (y = this.barCtx.getPreviousPath(l, c, !1)); var w = g.line(s, b); if (v = v + g.line(r, x) + g.line(r, b) + w + ("around" === d.config.plotOptions.bar.borderRadiusApplication ? " Z" : " z"), y = y + g.line(s, x) + w + w + w + w + w + g.line(s, x) + ("around" === d.config.plotOptions.bar.borderRadiusApplication ? " Z" : " z"), this.shouldApplyRadius(l) && (v = g.roundPathCorners(v, d.config.plotOptions.bar.borderRadius)), d.config.chart.stacked) { var k = this.barCtx; (k = this.barCtx[n]).xArrj.push(r + f), k.xArrjF.push(Math.abs(s - r)), k.xArrjVal.push(this.barCtx.series[h][c]) } return { pathTo: v, pathFrom: y } } }, { key: "checkZeroSeries", value: function (t) { for (var e = t.series, i = this.w, a = 0; a < e.length; a++) { for (var s = 0, r = 0; r < e[i.globals.maxValsInArrayIndex].length; r++)s += e[a][r]; 0 === s && this.barCtx.zeroSerieses.push(a) } } }, { key: "getXForValue", value: function (t, e) { var i = !(arguments.length > 2 && void 0 !== arguments[2]) || arguments[2] ? e : null; return null != t && (i = e + t / this.barCtx.invertedYRatio - 2 * (this.barCtx.isReversed ? t / this.barCtx.invertedYRatio : 0)), i } }, { key: "getYForValue", value: function (t, e, i) { var a = !(arguments.length > 3 && void 0 !== arguments[3]) || arguments[3] ? e : null; return null != t && (a = e - t / this.barCtx.yRatio[i] + 2 * (this.barCtx.isReversed ? t / this.barCtx.yRatio[i] : 0)), a } }, { key: "getGoalValues", value: function (t, i, a, s, r, n) { var l = this, h = this.w, c = [], d = function (e, s) { var r; c.push((o(r = {}, t, "x" === t ? l.getXForValue(e, i, !1) : l.getYForValue(e, a, n, !1)), o(r, "attrs", s), r)) }; if (h.globals.seriesGoals[s] && h.globals.seriesGoals[s][r] && Array.isArray(h.globals.seriesGoals[s][r]) && h.globals.seriesGoals[s][r].forEach((function (t) { d(t.value, t) })), this.barCtx.barOptions.isDumbbell && h.globals.seriesRange.length) { var g = this.barCtx.barOptions.dumbbellColors ? this.barCtx.barOptions.dumbbellColors : h.globals.colors, u = { strokeHeight: "x" === t ? 0 : h.globals.markers.size[s], strokeWidth: "x" === t ? h.globals.markers.size[s] : 0, strokeDashArray: 0, strokeLineCap: "round", strokeColor: Array.isArray(g[s]) ? g[s][0] : g[s] }; d(h.globals.seriesRangeStart[s][r], u), d(h.globals.seriesRangeEnd[s][r], e(e({}, u), {}, { strokeColor: Array.isArray(g[s]) ? g[s][1] : g[s] })) } return c } }, { key: "drawGoalLine", value: function (t) { var e = t.barXPosition, i = t.barYPosition, a = t.goalX, s = t.goalY, r = t.barWidth, o = t.barHeight, n = new m(this.barCtx.ctx), l = n.group({ className: "apexcharts-bar-goals-groups" }); l.node.classList.add("apexcharts-element-hidden"), this.barCtx.w.globals.delayedElements.push({ el: l.node }), l.attr("clip-path", "url(#gridRectMarkerMask".concat(this.barCtx.w.globals.cuid, ")")); var h = null; return this.barCtx.isHorizontal ? Array.isArray(a) && a.forEach((function (t) { if (t.x >= -1 && t.x <= n.w.globals.gridWidth + 1) { var e = void 0 !== t.attrs.strokeHeight ? t.attrs.strokeHeight : o / 2, a = i + e + o / 2; h = n.drawLine(t.x, a - 2 * e, t.x, a, t.attrs.strokeColor ? t.attrs.strokeColor : void 0, t.attrs.strokeDashArray, t.attrs.strokeWidth ? t.attrs.strokeWidth : 2, t.attrs.strokeLineCap), l.add(h) } })) : Array.isArray(s) && s.forEach((function (t) { if (t.y >= -1 && t.y <= n.w.globals.gridHeight + 1) { var i = void 0 !== t.attrs.strokeWidth ? t.attrs.strokeWidth : r / 2, a = e + i + r / 2; h = n.drawLine(a - 2 * i, t.y, a, t.y, t.attrs.strokeColor ? t.attrs.strokeColor : void 0, t.attrs.strokeDashArray, t.attrs.strokeHeight ? t.attrs.strokeHeight : 2, t.attrs.strokeLineCap), l.add(h) } })), l } }, { key: "drawBarShadow", value: function (t) { var e = t.prevPaths, i = t.currPaths, a = t.color, s = this.w, r = e.x, o = e.x1, n = e.barYPosition, l = i.x, h = i.x1, c = i.barYPosition, d = n + i.barHeight, g = new m(this.barCtx.ctx), u = new x, p = g.move(o, d) + g.line(r, d) + g.line(l, c) + g.line(h, c) + g.line(o, d) + ("around" === s.config.plotOptions.bar.borderRadiusApplication ? " Z" : " z"); return g.drawPath({ d: p, fill: u.shadeColor(.5, x.rgb2hex(a)), stroke: "none", strokeWidth: 0, fillOpacity: 1, classes: "apexcharts-bar-shadows" }) } }, { key: "getZeroValueEncounters", value: function (t) { var e, i = t.i, a = t.j, s = this.w, r = 0, o = 0; return (s.config.plotOptions.bar.horizontal ? s.globals.series.map((function (t, e) { return e })) : (null === (e = s.globals.columnSeries) || void 0 === e ? void 0 : e.i.map((function (t) { return t }))) || []).forEach((function (t) { var e = s.globals.seriesPercent[t][a]; e && r++, t < i && 0 === e && o++ })), { nonZeroColumns: r, zeroEncounters: o } } }, { key: "getGroupIndex", value: function (t) { var e = this.w, i = e.globals.seriesGroups.findIndex((function (i) { return i.indexOf(e.globals.seriesNames[t]) > -1 })), a = this.barCtx.columnGroupIndices, s = a.indexOf(i); return s < 0 && (a.push(i), s = a.length - 1), { groupIndex: i, columnGroupIndex: s } } }]), t }(), yt = function () { function t(e, i) { a(this, t), this.ctx = e, this.w = e.w; var s = this.w; this.barOptions = s.config.plotOptions.bar, this.isHorizontal = this.barOptions.horizontal, this.strokeWidth = s.config.stroke.width, this.isNullValue = !1, this.isRangeBar = s.globals.seriesRange.length && this.isHorizontal, this.isVerticalGroupedRangeBar = !s.globals.isBarHorizontal && s.globals.seriesRange.length && s.config.plotOptions.bar.rangeBarGroupRows, this.isFunnel = this.barOptions.isFunnel, this.xyRatios = i, null !== this.xyRatios && (this.xRatio = i.xRatio, this.yRatio = i.yRatio, this.invertedXRatio = i.invertedXRatio, this.invertedYRatio = i.invertedYRatio, this.baseLineY = i.baseLineY, this.baseLineInvertedY = i.baseLineInvertedY), this.yaxisIndex = 0, this.translationsIndex = 0, this.seriesLen = 0, this.pathArr = []; var r = new W(this.ctx); this.lastActiveBarSerieIndex = r.getActiveConfigSeriesIndex("desc", ["bar", "column"]), this.columnGroupIndices = []; var o = r.getBarSeriesIndices(), n = new y(this.ctx); this.stackedSeriesTotals = n.getStackedSeriesTotals(this.w.config.series.map((function (t, e) { return -1 === o.indexOf(e) ? e : -1 })).filter((function (t) { return -1 !== t }))), this.barHelpers = new mt(this) } return r(t, [{ key: "draw", value: function (t, i) { var a = this.w, s = new m(this.ctx), r = new y(this.ctx, a); t = r.getLogSeries(t), this.series = t, this.yRatio = r.getLogYRatios(this.yRatio), this.barHelpers.initVariables(t); var o = s.group({ class: "apexcharts-bar-series apexcharts-plot-series" }); a.config.dataLabels.enabled && this.totalItems > this.barOptions.dataLabels.maxItems && console.warn("WARNING: DataLabels are enabled but there are too many to display. This may cause performance issue when rendering - ApexCharts"); for (var n = 0, l = 0; n < t.length; n++, l++) { var h, c, d, g, u = void 0, p = void 0, f = [], b = [], v = a.globals.comboCharts ? i[n] : n, w = this.barHelpers.getGroupIndex(v).columnGroupIndex, k = s.group({ class: "apexcharts-series", rel: n + 1, seriesName: x.escapeString(a.globals.seriesNames[v]), "data:realIndex": v }); this.ctx.series.addCollapsedClassToSeries(k, v), t[n].length > 0 && (this.visibleI = this.visibleI + 1); var A = 0, S = 0; this.yRatio.length > 1 && (this.yaxisIndex = a.globals.seriesYAxisReverseMap[v], this.translationsIndex = v); var C = this.translationsIndex; this.isReversed = a.config.yaxis[this.yaxisIndex] && a.config.yaxis[this.yaxisIndex].reversed; var L = this.barHelpers.initialPositions(); p = L.y, A = L.barHeight, c = L.yDivision, g = L.zeroW, u = L.x, S = L.barWidth, h = L.xDivision, d = L.zeroH, this.horizontal || b.push(u + S / 2); var P = s.group({ class: "apexcharts-datalabels", "data:realIndex": v }); a.globals.delayedElements.push({ el: P.node }), P.node.classList.add("apexcharts-element-hidden"); var M = s.group({ class: "apexcharts-bar-goals-markers" }), I = s.group({ class: "apexcharts-bar-shadows" }); a.globals.delayedElements.push({ el: I.node }), I.node.classList.add("apexcharts-element-hidden"); for (var T = 0; T < t[n].length; T++) { var z = this.barHelpers.getStrokeWidth(n, T, v), X = null, E = { indexes: { i: n, j: T, realIndex: v, translationsIndex: C, bc: l }, x: u, y: p, strokeWidth: z, elSeries: k }; this.isHorizontal ? (X = this.drawBarPaths(e(e({}, E), {}, { barHeight: A, zeroW: g, yDivision: c })), S = this.series[n][T] / this.invertedYRatio) : (X = this.drawColumnPaths(e(e({}, E), {}, { xDivision: h, barWidth: S, zeroH: d })), A = this.series[n][T] / this.yRatio[C]); var Y = this.barHelpers.getPathFillColor(t, n, T, v); if (this.isFunnel && this.barOptions.isFunnel3d && this.pathArr.length && T > 0) { var F = this.barHelpers.drawBarShadow({ color: "string" == typeof Y && -1 === (null == Y ? void 0 : Y.indexOf("url")) ? Y : x.hexToRgba(a.globals.colors[n]), prevPaths: this.pathArr[this.pathArr.length - 1], currPaths: X }); F && I.add(F) } this.pathArr.push(X); var R = this.barHelpers.drawGoalLine({ barXPosition: X.barXPosition, barYPosition: X.barYPosition, goalX: X.goalX, goalY: X.goalY, barHeight: A, barWidth: S }); R && M.add(R), p = X.y, u = X.x, T > 0 && b.push(u + S / 2), f.push(p), this.renderSeries({ realIndex: v, pathFill: Y, j: T, i: n, columnGroupIndex: w, pathFrom: X.pathFrom, pathTo: X.pathTo, strokeWidth: z, elSeries: k, x: u, y: p, series: t, barHeight: X.barHeight ? X.barHeight : A, barWidth: X.barWidth ? X.barWidth : S, elDataLabelsWrap: P, elGoalsMarkers: M, elBarShadows: I, visibleSeries: this.visibleI, type: "bar" }) } a.globals.seriesXvalues[v] = b, a.globals.seriesYvalues[v] = f, o.add(k) } return o } }, { key: "renderSeries", value: function (t) { var e = t.realIndex, i = t.pathFill, a = t.lineFill, s = t.j, r = t.i, o = t.columnGroupIndex, n = t.pathFrom, l = t.pathTo, h = t.strokeWidth, c = t.elSeries, d = t.x, g = t.y, u = t.y1, p = t.y2, f = t.series, x = t.barHeight, b = t.barWidth, y = t.barXPosition, w = t.barYPosition, k = t.elDataLabelsWrap, A = t.elGoalsMarkers, S = t.elBarShadows, C = t.visibleSeries, L = t.type, P = this.w, M = new m(this.ctx); if (!a) { var I = "function" == typeof P.globals.stroke.colors[e] ? function (t) { var e, i = P.config.stroke.colors; return Array.isArray(i) && i.length > 0 && ((e = i[t]) || (e = ""), "function" == typeof e) ? e({ value: P.globals.series[t][s], dataPointIndex: s, w: P }) : e }(e) : P.globals.stroke.colors[e]; a = this.barOptions.distributed ? P.globals.stroke.colors[s] : I } P.config.series[r].data[s] && P.config.series[r].data[s].strokeColor && (a = P.config.series[r].data[s].strokeColor), this.isNullValue && (i = "none"); var T = s / P.config.chart.animations.animateGradually.delay * (P.config.chart.animations.speed / P.globals.dataPoints) / 2.4, z = M.renderPaths({ i: r, j: s, realIndex: e, pathFrom: n, pathTo: l, stroke: a, strokeWidth: h, strokeLineCap: P.config.stroke.lineCap, fill: i, animationDelay: T, initialSpeed: P.config.chart.animations.speed, dataChangeSpeed: P.config.chart.animations.dynamicAnimation.speed, className: "apexcharts-".concat(L, "-area") }); z.attr("clip-path", "url(#gridRectMask".concat(P.globals.cuid, ")")); var X = P.config.forecastDataPoints; X.count > 0 && s >= P.globals.dataPoints - X.count && (z.node.setAttribute("stroke-dasharray", X.dashArray), z.node.setAttribute("stroke-width", X.strokeWidth), z.node.setAttribute("fill-opacity", X.fillOpacity)), void 0 !== u && void 0 !== p && (z.attr("data-range-y1", u), z.attr("data-range-y2", p)), new v(this.ctx).setSelectionFilter(z, e, s), c.add(z); var E = new vt(this).handleBarDataLabels({ x: d, y: g, y1: u, y2: p, i: r, j: s, series: f, realIndex: e, columnGroupIndex: o, barHeight: x, barWidth: b, barXPosition: y, barYPosition: w, renderedPath: z, visibleSeries: C }); return null !== E.dataLabels && k.add(E.dataLabels), E.totalDataLabels && k.add(E.totalDataLabels), c.add(k), A && c.add(A), S && c.add(S), c } }, { key: "drawBarPaths", value: function (t) { var e, i = t.indexes, a = t.barHeight, s = t.strokeWidth, r = t.zeroW, o = t.x, n = t.y, l = t.yDivision, h = t.elSeries, c = this.w, d = i.i, g = i.j; if (c.globals.isXNumeric) e = (n = (c.globals.seriesX[d][g] - c.globals.minX) / this.invertedXRatio - a) + a * this.visibleI; else if (c.config.plotOptions.bar.hideZeroBarsWhenGrouped) { var u = 0, p = 0; c.globals.seriesPercent.forEach((function (t, e) { t[g] && u++, e < d && 0 === t[g] && p++ })), u > 0 && (a = this.seriesLen * a / u), e = n + a * this.visibleI, e -= a * p } else e = n + a * this.visibleI; this.isFunnel && (r -= (this.barHelpers.getXForValue(this.series[d][g], r) - r) / 2), o = this.barHelpers.getXForValue(this.series[d][g], r); var f = this.barHelpers.getBarpaths({ barYPosition: e, barHeight: a, x1: r, x2: o, strokeWidth: s, series: this.series, realIndex: i.realIndex, i: d, j: g, w: c }); return c.globals.isXNumeric || (n += l), this.barHelpers.barBackground({ j: g, i: d, y1: e - a * this.visibleI, y2: a * this.seriesLen, elSeries: h }), { pathTo: f.pathTo, pathFrom: f.pathFrom, x1: r, x: o, y: n, goalX: this.barHelpers.getGoalValues("x", r, null, d, g), barYPosition: e, barHeight: a } } }, { key: "drawColumnPaths", value: function (t) { var e, i = t.indexes, a = t.x, s = t.y, r = t.xDivision, o = t.barWidth, n = t.zeroH, l = t.strokeWidth, h = t.elSeries, c = this.w, d = i.realIndex, g = i.translationsIndex, u = i.i, p = i.j, f = i.bc; if (c.globals.isXNumeric) { var x = this.getBarXForNumericXAxis({ x: a, j: p, realIndex: d, barWidth: o }); a = x.x, e = x.barXPosition } else if (c.config.plotOptions.bar.hideZeroBarsWhenGrouped) { var b = this.barHelpers.getZeroValueEncounters({ i: u, j: p }), v = b.nonZeroColumns, m = b.zeroEncounters; v > 0 && (o = this.seriesLen * o / v), e = a + o * this.visibleI, e -= o * m } else e = a + o * this.visibleI; s = this.barHelpers.getYForValue(this.series[u][p], n, g); var y = this.barHelpers.getColumnPaths({ barXPosition: e, barWidth: o, y1: n, y2: s, strokeWidth: l, series: this.series, realIndex: d, i: u, j: p, w: c }); return c.globals.isXNumeric || (a += r), this.barHelpers.barBackground({ bc: f, j: p, i: u, x1: e - l / 2 - o * this.visibleI, x2: o * this.seriesLen + l / 2, elSeries: h }), { pathTo: y.pathTo, pathFrom: y.pathFrom, x: a, y: s, goalY: this.barHelpers.getGoalValues("y", null, n, u, p, g), barXPosition: e, barWidth: o } } }, { key: "getBarXForNumericXAxis", value: function (t) { var e = t.x, i = t.barWidth, a = t.realIndex, s = t.j, r = this.w, o = a; return r.globals.seriesX[a].length || (o = r.globals.maxValsInArrayIndex), r.globals.seriesX[o][s] && (e = (r.globals.seriesX[o][s] - r.globals.minX) / this.xRatio - i * this.seriesLen / 2), { barXPosition: e + i * this.visibleI, x: e } } }, { key: "getPreviousPath", value: function (t, e) { for (var i, a = this.w, s = 0; s < a.globals.previousPaths.length; s++) { var r = a.globals.previousPaths[s]; r.paths && r.paths.length > 0 && parseInt(r.realIndex, 10) === parseInt(t, 10) && void 0 !== a.globals.previousPaths[s].paths[e] && (i = a.globals.previousPaths[s].paths[e].d) } return i } }]), t }(), wt = function (t) { n(s, t); var i = d(s); function s() { return a(this, s), i.apply(this, arguments) } return r(s, [{ key: "draw", value: function (t, i) { var a = this, s = this.w; this.graphics = new m(this.ctx), this.bar = new yt(this.ctx, this.xyRatios); var r = new y(this.ctx, s); t = r.getLogSeries(t), this.yRatio = r.getLogYRatios(this.yRatio), this.barHelpers.initVariables(t), "100%" === s.config.chart.stackType && (t = s.globals.comboCharts ? i.map((function (t) { return s.globals.seriesPercent[t] })) : s.globals.seriesPercent.slice()), this.series = t, this.barHelpers.initializeStackedPrevVars(this); for (var o = this.graphics.group({ class: "apexcharts-bar-series apexcharts-plot-series" }), n = 0, l = 0, h = function (r, h) { var c = void 0, d = void 0, g = void 0, u = void 0, p = s.globals.comboCharts ? i[r] : r, f = a.barHelpers.getGroupIndex(p), b = f.groupIndex, v = f.columnGroupIndex; a.groupCtx = a[s.globals.seriesGroups[b]]; var m = [], y = [], w = 0; a.yRatio.length > 1 && (a.yaxisIndex = s.globals.seriesYAxisReverseMap[p][0], w = p), a.isReversed = s.config.yaxis[a.yaxisIndex] && s.config.yaxis[a.yaxisIndex].reversed; var k = a.graphics.group({ class: "apexcharts-series", seriesName: x.escapeString(s.globals.seriesNames[p]), rel: r + 1, "data:realIndex": p }); a.ctx.series.addCollapsedClassToSeries(k, p); var A = a.graphics.group({ class: "apexcharts-datalabels", "data:realIndex": p }), S = a.graphics.group({ class: "apexcharts-bar-goals-markers" }), C = 0, L = 0, P = a.initialPositions(n, l, c, d, g, u, w); l = P.y, C = P.barHeight, d = P.yDivision, u = P.zeroW, n = P.x, L = P.barWidth, c = P.xDivision, g = P.zeroH, s.globals.barHeight = C, s.globals.barWidth = L, a.barHelpers.initializeStackedXYVars(a), 1 === a.groupCtx.prevY.length && a.groupCtx.prevY[0].every((function (t) { return isNaN(t) })) && (a.groupCtx.prevY[0] = a.groupCtx.prevY[0].map((function () { return g })), a.groupCtx.prevYF[0] = a.groupCtx.prevYF[0].map((function () { return 0 }))); for (var M = 0; M < s.globals.dataPoints; M++) { var I = a.barHelpers.getStrokeWidth(r, M, p), T = { indexes: { i: r, j: M, realIndex: p, translationsIndex: w, bc: h }, strokeWidth: I, x: n, y: l, elSeries: k, columnGroupIndex: v, seriesGroup: s.globals.seriesGroups[b] }, z = null; a.isHorizontal ? (z = a.drawStackedBarPaths(e(e({}, T), {}, { zeroW: u, barHeight: C, yDivision: d })), L = a.series[r][M] / a.invertedYRatio) : (z = a.drawStackedColumnPaths(e(e({}, T), {}, { xDivision: c, barWidth: L, zeroH: g })), C = a.series[r][M] / a.yRatio[w]); var X = a.barHelpers.drawGoalLine({ barXPosition: z.barXPosition, barYPosition: z.barYPosition, goalX: z.goalX, goalY: z.goalY, barHeight: C, barWidth: L }); X && S.add(X), l = z.y, n = z.x, m.push(n), y.push(l); var E = a.barHelpers.getPathFillColor(t, r, M, p); k = a.renderSeries({ realIndex: p, pathFill: E, j: M, i: r, columnGroupIndex: v, pathFrom: z.pathFrom, pathTo: z.pathTo, strokeWidth: I, elSeries: k, x: n, y: l, series: t, barHeight: C, barWidth: L, elDataLabelsWrap: A, elGoalsMarkers: S, type: "bar", visibleSeries: 0 }) } s.globals.seriesXvalues[p] = m, s.globals.seriesYvalues[p] = y, a.groupCtx.prevY.push(a.groupCtx.yArrj), a.groupCtx.prevYF.push(a.groupCtx.yArrjF), a.groupCtx.prevYVal.push(a.groupCtx.yArrjVal), a.groupCtx.prevX.push(a.groupCtx.xArrj), a.groupCtx.prevXF.push(a.groupCtx.xArrjF), a.groupCtx.prevXVal.push(a.groupCtx.xArrjVal), o.add(k) }, c = 0, d = 0; c < t.length; c++, d++)h(c, d); return o } }, { key: "initialPositions", value: function (t, e, i, a, s, r, o) { var n, l, h = this.w; if (this.isHorizontal) { a = h.globals.gridHeight / h.globals.dataPoints; var c = h.config.plotOptions.bar.barHeight; n = -1 === String(c).indexOf("%") ? parseInt(c, 10) : a * parseInt(c, 10) / 100, r = h.globals.padHorizontal + (this.isReversed ? h.globals.gridWidth - this.baseLineInvertedY : this.baseLineInvertedY), e = (a - n) / 2 } else { l = i = h.globals.gridWidth / h.globals.dataPoints; var d = h.config.plotOptions.bar.columnWidth; h.globals.isXNumeric && h.globals.dataPoints > 1 ? l = (i = h.globals.minXDiff / this.xRatio) * parseInt(this.barOptions.columnWidth, 10) / 100 : -1 === String(d).indexOf("%") ? l = parseInt(d, 10) : l *= parseInt(d, 10) / 100, s = h.globals.gridHeight - this.baseLineY[o] - (this.isReversed ? h.globals.gridHeight : 0), t = h.globals.padHorizontal + (i - l) / 2 } var g = h.globals.barGroups.length || 1; return { x: t, y: e, yDivision: a, xDivision: i, barHeight: n / g, barWidth: l / g, zeroH: s, zeroW: r } } }, { key: "drawStackedBarPaths", value: function (t) { for (var e, i = t.indexes, a = t.barHeight, s = t.strokeWidth, r = t.zeroW, o = t.x, n = t.y, l = t.columnGroupIndex, h = t.seriesGroup, c = t.yDivision, d = t.elSeries, g = this.w, u = n + l * a, p = i.i, f = i.j, x = i.realIndex, b = i.translationsIndex, v = 0, m = 0; m < this.groupCtx.prevXF.length; m++)v += this.groupCtx.prevXF[m][f]; var y; if ((y = h.indexOf(g.config.series[x].name)) > 0) { var w = r; this.groupCtx.prevXVal[y - 1][f] < 0 ? w = this.series[p][f] >= 0 ? this.groupCtx.prevX[y - 1][f] + v - 2 * (this.isReversed ? v : 0) : this.groupCtx.prevX[y - 1][f] : this.groupCtx.prevXVal[y - 1][f] >= 0 && (w = this.series[p][f] >= 0 ? this.groupCtx.prevX[y - 1][f] : this.groupCtx.prevX[y - 1][f] - v + 2 * (this.isReversed ? v : 0)), e = w } else e = r; o = null === this.series[p][f] ? e : e + this.series[p][f] / this.invertedYRatio - 2 * (this.isReversed ? this.series[p][f] / this.invertedYRatio : 0); var k = this.barHelpers.getBarpaths({ barYPosition: u, barHeight: a, x1: e, x2: o, strokeWidth: s, series: this.series, realIndex: i.realIndex, seriesGroup: h, i: p, j: f, w: g }); return this.barHelpers.barBackground({ j: f, i: p, y1: u, y2: a, elSeries: d }), n += c, { pathTo: k.pathTo, pathFrom: k.pathFrom, goalX: this.barHelpers.getGoalValues("x", r, null, p, f, b), barXPosition: e, barYPosition: u, x: o, y: n } } }, { key: "drawStackedColumnPaths", value: function (t) { var e = t.indexes, i = t.x, a = t.y, s = t.xDivision, r = t.barWidth, o = t.zeroH, n = t.columnGroupIndex, l = t.seriesGroup, h = t.elSeries, c = this.w, d = e.i, g = e.j, u = e.bc, p = e.realIndex, f = e.translationsIndex; if (c.globals.isXNumeric) { var x = c.globals.seriesX[p][g]; x || (x = 0), i = (x - c.globals.minX) / this.xRatio - r / 2 * c.globals.barGroups.length } for (var b, v = i + n * r, m = 0, y = 0; y < this.groupCtx.prevYF.length; y++)m += isNaN(this.groupCtx.prevYF[y][g]) ? 0 : this.groupCtx.prevYF[y][g]; var w = d; if (l && (w = l.indexOf(c.globals.seriesNames[p])), w > 0 && !c.globals.isXNumeric || w > 0 && c.globals.isXNumeric && c.globals.seriesX[p - 1][g] === c.globals.seriesX[p][g]) { var k, A, S, C = Math.min(this.yRatio.length + 1, p + 1); if (void 0 !== this.groupCtx.prevY[w - 1] && this.groupCtx.prevY[w - 1].length) for (var L = 1; L < C; L++) { var P; if (!isNaN(null === (P = this.groupCtx.prevY[w - L]) || void 0 === P ? void 0 : P[g])) { S = this.groupCtx.prevY[w - L][g]; break } } for (var M = 1; M < C; M++) { var I, T; if ((null === (I = this.groupCtx.prevYVal[w - M]) || void 0 === I ? void 0 : I[g]) < 0) { A = this.series[d][g] >= 0 ? S - m + 2 * (this.isReversed ? m : 0) : S; break } if ((null === (T = this.groupCtx.prevYVal[w - M]) || void 0 === T ? void 0 : T[g]) >= 0) { A = this.series[d][g] >= 0 ? S : S + m - 2 * (this.isReversed ? m : 0); break } } void 0 === A && (A = c.globals.gridHeight), b = null !== (k = this.groupCtx.prevYF[0]) && void 0 !== k && k.every((function (t) { return 0 === t })) && this.groupCtx.prevYF.slice(1, w).every((function (t) { return t.every((function (t) { return isNaN(t) })) })) ? o : A } else b = o; a = this.series[d][g] ? b - this.series[d][g] / this.yRatio[f] + 2 * (this.isReversed ? this.series[d][g] / this.yRatio[f] : 0) : b; var z = this.barHelpers.getColumnPaths({ barXPosition: v, barWidth: r, y1: b, y2: a, yRatio: this.yRatio[f], strokeWidth: this.strokeWidth, series: this.series, seriesGroup: l, realIndex: e.realIndex, i: d, j: g, w: c }); return this.barHelpers.barBackground({ bc: u, j: g, i: d, x1: v, x2: r, elSeries: h }), i += s, { pathTo: z.pathTo, pathFrom: z.pathFrom, goalY: this.barHelpers.getGoalValues("y", null, o, d, g), barXPosition: v, x: c.globals.isXNumeric ? i - s : i, y: a } } }]), s }(yt), kt = function (t) { n(s, t); var i = d(s); function s() { return a(this, s), i.apply(this, arguments) } return r(s, [{ key: "draw", value: function (t, i, a) { var s = this, r = this.w, o = new m(this.ctx), n = r.globals.comboCharts ? i : r.config.chart.type, l = new H(this.ctx); this.candlestickOptions = this.w.config.plotOptions.candlestick, this.boxOptions = this.w.config.plotOptions.boxPlot, this.isHorizontal = r.config.plotOptions.bar.horizontal; var h = new y(this.ctx, r); t = h.getLogSeries(t), this.series = t, this.yRatio = h.getLogYRatios(this.yRatio), this.barHelpers.initVariables(t); for (var c = o.group({ class: "apexcharts-".concat(n, "-series apexcharts-plot-series") }), d = function (i) { s.isBoxPlot = "boxPlot" === r.config.chart.type || "boxPlot" === r.config.series[i].type; var n, h, d, g, u = void 0, p = void 0, f = [], b = [], v = r.globals.comboCharts ? a[i] : i, m = s.barHelpers.getGroupIndex(v).columnGroupIndex, y = o.group({ class: "apexcharts-series", seriesName: x.escapeString(r.globals.seriesNames[v]), rel: i + 1, "data:realIndex": v }); s.ctx.series.addCollapsedClassToSeries(y, v), t[i].length > 0 && (s.visibleI = s.visibleI + 1); var w, k, A = 0; s.yRatio.length > 1 && (s.yaxisIndex = r.globals.seriesYAxisReverseMap[v][0], A = v); var S = s.barHelpers.initialPositions(); p = S.y, w = S.barHeight, h = S.yDivision, g = S.zeroW, u = S.x, k = S.barWidth, n = S.xDivision, d = S.zeroH, b.push(u + k / 2); for (var C = o.group({ class: "apexcharts-datalabels", "data:realIndex": v }), L = function (a) { var o = s.barHelpers.getStrokeWidth(i, a, v), c = null, x = { indexes: { i: i, j: a, realIndex: v, translationsIndex: A }, x: u, y: p, strokeWidth: o, elSeries: y }; c = s.isHorizontal ? s.drawHorizontalBoxPaths(e(e({}, x), {}, { yDivision: h, barHeight: w, zeroW: g })) : s.drawVerticalBoxPaths(e(e({}, x), {}, { xDivision: n, barWidth: k, zeroH: d })), p = c.y, u = c.x, a > 0 && b.push(u + k / 2), f.push(p), c.pathTo.forEach((function (e, n) { var h = !s.isBoxPlot && s.candlestickOptions.wick.useFillColor ? c.color[n] : r.globals.stroke.colors[i], d = l.fillPath({ seriesNumber: v, dataPointIndex: a, color: c.color[n], value: t[i][a] }); s.renderSeries({ realIndex: v, pathFill: d, lineFill: h, j: a, i: i, pathFrom: c.pathFrom, pathTo: e, strokeWidth: o, elSeries: y, x: u, y: p, series: t, columnGroupIndex: m, barHeight: w, barWidth: k, elDataLabelsWrap: C, visibleSeries: s.visibleI, type: r.config.chart.type }) })) }, P = 0; P < r.globals.dataPoints; P++)L(P); r.globals.seriesXvalues[v] = b, r.globals.seriesYvalues[v] = f, c.add(y) }, g = 0; g < t.length; g++)d(g); return c } }, { key: "drawVerticalBoxPaths", value: function (t) { var e = t.indexes, i = t.x; t.y; var a = t.xDivision, s = t.barWidth, r = t.zeroH, o = t.strokeWidth, n = this.w, l = new m(this.ctx), h = e.i, c = e.j, d = !0, g = n.config.plotOptions.candlestick.colors.upward, u = n.config.plotOptions.candlestick.colors.downward, p = ""; this.isBoxPlot && (p = [this.boxOptions.colors.lower, this.boxOptions.colors.upper]); var f = this.yRatio[e.translationsIndex], x = e.realIndex, b = this.getOHLCValue(x, c), v = r, y = r; b.o > b.c && (d = !1); var w = Math.min(b.o, b.c), k = Math.max(b.o, b.c), A = b.m; n.globals.isXNumeric && (i = (n.globals.seriesX[x][c] - n.globals.minX) / this.xRatio - s / 2); var S = i + s * this.visibleI; void 0 === this.series[h][c] || null === this.series[h][c] ? (w = r, k = r) : (w = r - w / f, k = r - k / f, v = r - b.h / f, y = r - b.l / f, A = r - b.m / f); var C = l.move(S, r), L = l.move(S + s / 2, w); return n.globals.previousPaths.length > 0 && (L = this.getPreviousPath(x, c, !0)), C = this.isBoxPlot ? [l.move(S, w) + l.line(S + s / 2, w) + l.line(S + s / 2, v) + l.line(S + s / 4, v) + l.line(S + s - s / 4, v) + l.line(S + s / 2, v) + l.line(S + s / 2, w) + l.line(S + s, w) + l.line(S + s, A) + l.line(S, A) + l.line(S, w + o / 2), l.move(S, A) + l.line(S + s, A) + l.line(S + s, k) + l.line(S + s / 2, k) + l.line(S + s / 2, y) + l.line(S + s - s / 4, y) + l.line(S + s / 4, y) + l.line(S + s / 2, y) + l.line(S + s / 2, k) + l.line(S, k) + l.line(S, A) + "z"] : [l.move(S, k) + l.line(S + s / 2, k) + l.line(S + s / 2, v) + l.line(S + s / 2, k) + l.line(S + s, k) + l.line(S + s, w) + l.line(S + s / 2, w) + l.line(S + s / 2, y) + l.line(S + s / 2, w) + l.line(S, w) + l.line(S, k - o / 2)], L += l.move(S, w), n.globals.isXNumeric || (i += a), { pathTo: C, pathFrom: L, x: i, y: k, barXPosition: S, color: this.isBoxPlot ? p : d ? [g] : [u] } } }, { key: "drawHorizontalBoxPaths", value: function (t) { var e = t.indexes; t.x; var i = t.y, a = t.yDivision, s = t.barHeight, r = t.zeroW, o = t.strokeWidth, n = this.w, l = new m(this.ctx), h = e.i, c = e.j, d = this.boxOptions.colors.lower; this.isBoxPlot && (d = [this.boxOptions.colors.lower, this.boxOptions.colors.upper]); var g = this.invertedYRatio, u = e.realIndex, p = this.getOHLCValue(u, c), f = r, x = r, b = Math.min(p.o, p.c), v = Math.max(p.o, p.c), y = p.m; n.globals.isXNumeric && (i = (n.globals.seriesX[u][c] - n.globals.minX) / this.invertedXRatio - s / 2); var w = i + s * this.visibleI; void 0 === this.series[h][c] || null === this.series[h][c] ? (b = r, v = r) : (b = r + b / g, v = r + v / g, f = r + p.h / g, x = r + p.l / g, y = r + p.m / g); var k = l.move(r, w), A = l.move(b, w + s / 2); return n.globals.previousPaths.length > 0 && (A = this.getPreviousPath(u, c, !0)), k = [l.move(b, w) + l.line(b, w + s / 2) + l.line(f, w + s / 2) + l.line(f, w + s / 2 - s / 4) + l.line(f, w + s / 2 + s / 4) + l.line(f, w + s / 2) + l.line(b, w + s / 2) + l.line(b, w + s) + l.line(y, w + s) + l.line(y, w) + l.line(b + o / 2, w), l.move(y, w) + l.line(y, w + s) + l.line(v, w + s) + l.line(v, w + s / 2) + l.line(x, w + s / 2) + l.line(x, w + s - s / 4) + l.line(x, w + s / 4) + l.line(x, w + s / 2) + l.line(v, w + s / 2) + l.line(v, w) + l.line(y, w) + "z"], A += l.move(b, w), n.globals.isXNumeric || (i += a), { pathTo: k, pathFrom: A, x: v, y: i, barYPosition: w, color: d } } }, { key: "getOHLCValue", value: function (t, e) { var i = this.w; return { o: this.isBoxPlot ? i.globals.seriesCandleH[t][e] : i.globals.seriesCandleO[t][e], h: this.isBoxPlot ? i.globals.seriesCandleO[t][e] : i.globals.seriesCandleH[t][e], m: i.globals.seriesCandleM[t][e], l: this.isBoxPlot ? i.globals.seriesCandleC[t][e] : i.globals.seriesCandleL[t][e], c: this.isBoxPlot ? i.globals.seriesCandleL[t][e] : i.globals.seriesCandleC[t][e] } } }]), s }(yt), At = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w } return r(t, [{ key: "checkColorRange", value: function () { var t = this.w, e = !1, i = t.config.plotOptions[t.config.chart.type]; return i.colorScale.ranges.length > 0 && i.colorScale.ranges.map((function (t, i) { t.from <= 0 && (e = !0) })), e } }, { key: "getShadeColor", value: function (t, e, i, a) { var s = this.w, r = 1, o = s.config.plotOptions[t].shadeIntensity, n = this.determineColor(t, e, i); s.globals.hasNegs || a ? r = s.config.plotOptions[t].reverseNegativeShade ? n.percent < 0 ? n.percent / 100 * (1.25 * o) : (1 - n.percent / 100) * (1.25 * o) : n.percent <= 0 ? 1 - (1 + n.percent / 100) * o : (1 - n.percent / 100) * o : (r = 1 - n.percent / 100, "treemap" === t && (r = (1 - n.percent / 100) * (1.25 * o))); var l = n.color, h = new x; return s.config.plotOptions[t].enableShades && (l = "dark" === this.w.config.theme.mode ? x.hexToRgba(h.shadeColor(-1 * r, n.color), s.config.fill.opacity) : x.hexToRgba(h.shadeColor(r, n.color), s.config.fill.opacity)), { color: l, colorProps: n } } }, { key: "determineColor", value: function (t, e, i) { var a = this.w, s = a.globals.series[e][i], r = a.config.plotOptions[t], o = r.colorScale.inverse ? i : e; r.distributed && "treemap" === a.config.chart.type && (o = i); var n = a.globals.colors[o], l = null, h = Math.min.apply(Math, u(a.globals.series[e])), c = Math.max.apply(Math, u(a.globals.series[e])); r.distributed || "heatmap" !== t || (h = a.globals.minY, c = a.globals.maxY), void 0 !== r.colorScale.min && (h = r.colorScale.min < a.globals.minY ? r.colorScale.min : a.globals.minY, c = r.colorScale.max > a.globals.maxY ? r.colorScale.max : a.globals.maxY); var d = Math.abs(c) + Math.abs(h), g = 100 * s / (0 === d ? d - 1e-6 : d); r.colorScale.ranges.length > 0 && r.colorScale.ranges.map((function (t, e) { if (s >= t.from && s <= t.to) { n = t.color, l = t.foreColor ? t.foreColor : null, h = t.from, c = t.to; var i = Math.abs(c) + Math.abs(h); g = 100 * s / (0 === i ? i - 1e-6 : i) } })); return { color: n, foreColor: l, percent: g } } }, { key: "calculateDataLabels", value: function (t) { var e = t.text, i = t.x, a = t.y, s = t.i, r = t.j, o = t.colorProps, n = t.fontSize, l = this.w.config.dataLabels, h = new m(this.ctx), c = new N(this.ctx), d = null; if (l.enabled) { d = h.group({ class: "apexcharts-data-labels" }); var g = l.offsetX, u = l.offsetY, p = i + g, f = a + parseFloat(l.style.fontSize) / 3 + u; c.plotDataLabelsText({ x: p, y: f, text: e, i: s, j: r, color: o.foreColor, parent: d, fontSize: n, dataLabelsConfig: l }) } return d } }, { key: "addListeners", value: function (t) { var e = new m(this.ctx); t.node.addEventListener("mouseenter", e.pathMouseEnter.bind(this, t)), t.node.addEventListener("mouseleave", e.pathMouseLeave.bind(this, t)), t.node.addEventListener("mousedown", e.pathMouseDown.bind(this, t)) } }]), t }(), St = function () { function t(e, i) { a(this, t), this.ctx = e, this.w = e.w, this.xRatio = i.xRatio, this.yRatio = i.yRatio, this.dynamicAnim = this.w.config.chart.animations.dynamicAnimation, this.helpers = new At(e), this.rectRadius = this.w.config.plotOptions.heatmap.radius, this.strokeWidth = this.w.config.stroke.show ? this.w.config.stroke.width : 0 } return r(t, [{ key: "draw", value: function (t) { var e = this.w, i = new m(this.ctx), a = i.group({ class: "apexcharts-heatmap" }); a.attr("clip-path", "url(#gridRectMask".concat(e.globals.cuid, ")")); var s = e.globals.gridWidth / e.globals.dataPoints, r = e.globals.gridHeight / e.globals.series.length, o = 0, n = !1; this.negRange = this.helpers.checkColorRange(); var l = t.slice(); e.config.yaxis[0].reversed && (n = !0, l.reverse()); for (var h = n ? 0 : l.length - 1; n ? h < l.length : h >= 0; n ? h++ : h--) { var c = i.group({ class: "apexcharts-series apexcharts-heatmap-series", seriesName: x.escapeString(e.globals.seriesNames[h]), rel: h + 1, "data:realIndex": h }); if (this.ctx.series.addCollapsedClassToSeries(c, h), e.config.chart.dropShadow.enabled) { var d = e.config.chart.dropShadow; new v(this.ctx).dropShadow(c, d, h) } for (var g = 0, u = e.config.plotOptions.heatmap.shadeIntensity, p = 0; p < l[h].length; p++) { var f = this.helpers.getShadeColor(e.config.chart.type, h, p, this.negRange), b = f.color, y = f.colorProps; if ("image" === e.config.fill.type) b = new H(this.ctx).fillPath({ seriesNumber: h, dataPointIndex: p, opacity: e.globals.hasNegs ? y.percent < 0 ? 1 - (1 + y.percent / 100) : u + y.percent / 100 : y.percent / 100, patternID: x.randomId(), width: e.config.fill.image.width ? e.config.fill.image.width : s, height: e.config.fill.image.height ? e.config.fill.image.height : r }); var w = this.rectRadius, k = i.drawRect(g, o, s, r, w); if (k.attr({ cx: g, cy: o }), k.node.classList.add("apexcharts-heatmap-rect"), c.add(k), k.attr({ fill: b, i: h, index: h, j: p, val: t[h][p], "stroke-width": this.strokeWidth, stroke: e.config.plotOptions.heatmap.useFillColorAsStroke ? b : e.globals.stroke.colors[0], color: b }), this.helpers.addListeners(k), e.config.chart.animations.enabled && !e.globals.dataChanged) { var A = 1; e.globals.resized || (A = e.config.chart.animations.speed), this.animateHeatMap(k, g, o, s, r, A) } if (e.globals.dataChanged) { var S = 1; if (this.dynamicAnim.enabled && e.globals.shouldAnimate) { S = this.dynamicAnim.speed; var C = e.globals.previousPaths[h] && e.globals.previousPaths[h][p] && e.globals.previousPaths[h][p].color; C || (C = "rgba(255, 255, 255, 0)"), this.animateHeatColor(k, x.isColorHex(C) ? C : x.rgb2hex(C), x.isColorHex(b) ? b : x.rgb2hex(b), S) } } var L = (0, e.config.dataLabels.formatter)(e.globals.series[h][p], { value: e.globals.series[h][p], seriesIndex: h, dataPointIndex: p, w: e }), P = this.helpers.calculateDataLabels({ text: L, x: g + s / 2, y: o + r / 2, i: h, j: p, colorProps: y, series: l }); null !== P && c.add(P), g += s } o += r, a.add(c) } var M = e.globals.yAxisScale[0].result.slice(); return e.config.yaxis[0].reversed ? M.unshift("") : M.push(""), e.globals.yAxisScale[0].result = M, a } }, { key: "animateHeatMap", value: function (t, e, i, a, s, r) { var o = new b(this.ctx); o.animateRect(t, { x: e + a / 2, y: i + s / 2, width: 0, height: 0 }, { x: e, y: i, width: a, height: s }, r, (function () { o.animationCompleted(t) })) } }, { key: "animateHeatColor", value: function (t, e, i, a) { t.attr({ fill: e }).animate(a).attr({ fill: i }) } }]), t }(), Ct = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w } return r(t, [{ key: "drawYAxisTexts", value: function (t, e, i, a) { var s = this.w, r = s.config.yaxis[0], o = s.globals.yLabelFormatters[0]; return new m(this.ctx).drawText({ x: t + r.labels.offsetX, y: e + r.labels.offsetY, text: o(a, i), textAnchor: "middle", fontSize: r.labels.style.fontSize, fontFamily: r.labels.style.fontFamily, foreColor: Array.isArray(r.labels.style.colors) ? r.labels.style.colors[i] : r.labels.style.colors }) } }]), t }(), Lt = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w; var i = this.w; this.chartType = this.w.config.chart.type, this.initialAnim = this.w.config.chart.animations.enabled, this.dynamicAnim = this.initialAnim && this.w.config.chart.animations.dynamicAnimation.enabled, this.animBeginArr = [0], this.animDur = 0, this.donutDataLabels = this.w.config.plotOptions.pie.donut.labels, this.lineColorArr = void 0 !== i.globals.stroke.colors ? i.globals.stroke.colors : i.globals.colors, this.defaultSize = Math.min(i.globals.gridWidth, i.globals.gridHeight), this.centerY = this.defaultSize / 2, this.centerX = i.globals.gridWidth / 2, "radialBar" === i.config.chart.type ? this.fullAngle = 360 : this.fullAngle = Math.abs(i.config.plotOptions.pie.endAngle - i.config.plotOptions.pie.startAngle), this.initialAngle = i.config.plotOptions.pie.startAngle % this.fullAngle, i.globals.radialSize = this.defaultSize / 2.05 - i.config.stroke.width - (i.config.chart.sparkline.enabled ? 0 : i.config.chart.dropShadow.blur), this.donutSize = i.globals.radialSize * parseInt(i.config.plotOptions.pie.donut.size, 10) / 100, this.maxY = 0, this.sliceLabels = [], this.sliceSizes = [], this.prevSectorAngleArr = [] } return r(t, [{ key: "draw", value: function (t) { var e = this, i = this.w, a = new m(this.ctx); if (this.ret = a.group({ class: "apexcharts-pie" }), i.globals.noData) return this.ret; for (var s = 0, r = 0; r < t.length; r++)s += x.negToZero(t[r]); var o = [], n = a.group(); 0 === s && (s = 1e-5), t.forEach((function (t) { e.maxY = Math.max(e.maxY, t) })), i.config.yaxis[0].max && (this.maxY = i.config.yaxis[0].max), "back" === i.config.grid.position && "polarArea" === this.chartType && this.drawPolarElements(this.ret); for (var l = 0; l < t.length; l++) { var h = this.fullAngle * x.negToZero(t[l]) / s; o.push(h), "polarArea" === this.chartType ? (o[l] = this.fullAngle / t.length, this.sliceSizes.push(i.globals.radialSize * t[l] / this.maxY)) : this.sliceSizes.push(i.globals.radialSize) } if (i.globals.dataChanged) { for (var c, d = 0, g = 0; g < i.globals.previousPaths.length; g++)d += x.negToZero(i.globals.previousPaths[g]); for (var u = 0; u < i.globals.previousPaths.length; u++)c = this.fullAngle * x.negToZero(i.globals.previousPaths[u]) / d, this.prevSectorAngleArr.push(c) } this.donutSize < 0 && (this.donutSize = 0); var p = i.config.plotOptions.pie.customScale, f = i.globals.gridWidth / 2, b = i.globals.gridHeight / 2, v = f - i.globals.gridWidth / 2 * p, y = b - i.globals.gridHeight / 2 * p; if ("donut" === this.chartType) { var w = a.drawCircle(this.donutSize); w.attr({ cx: this.centerX, cy: this.centerY, fill: i.config.plotOptions.pie.donut.background ? i.config.plotOptions.pie.donut.background : "transparent" }), n.add(w) } var k = this.drawArcs(o, t); if (this.sliceLabels.forEach((function (t) { k.add(t) })), n.attr({ transform: "translate(".concat(v, ", ").concat(y, ") scale(").concat(p, ")") }), n.add(k), this.ret.add(n), this.donutDataLabels.show) { var A = this.renderInnerDataLabels(this.donutDataLabels, { hollowSize: this.donutSize, centerX: this.centerX, centerY: this.centerY, opacity: this.donutDataLabels.show, translateX: v, translateY: y }); this.ret.add(A) } return "front" === i.config.grid.position && "polarArea" === this.chartType && this.drawPolarElements(this.ret), this.ret } }, { key: "drawArcs", value: function (t, e) { var i = this.w, a = new v(this.ctx), s = new m(this.ctx), r = new H(this.ctx), o = s.group({ class: "apexcharts-slices" }), n = this.initialAngle, l = this.initialAngle, h = this.initialAngle, c = this.initialAngle; this.strokeWidth = i.config.stroke.show ? i.config.stroke.width : 0; for (var d = 0; d < t.length; d++) { var g = s.group({ class: "apexcharts-series apexcharts-pie-series", seriesName: x.escapeString(i.globals.seriesNames[d]), rel: d + 1, "data:realIndex": d }); o.add(g), l = c, h = (n = h) + t[d], c = l + this.prevSectorAngleArr[d]; var u = h < n ? this.fullAngle + h - n : h - n, p = r.fillPath({ seriesNumber: d, size: this.sliceSizes[d], value: e[d] }), f = this.getChangedPath(l, c), b = s.drawPath({ d: f, stroke: Array.isArray(this.lineColorArr) ? this.lineColorArr[d] : this.lineColorArr, strokeWidth: 0, fill: p, fillOpacity: i.config.fill.opacity, classes: "apexcharts-pie-area apexcharts-".concat(this.chartType.toLowerCase(), "-slice-").concat(d) }); if (b.attr({ index: 0, j: d }), a.setSelectionFilter(b, 0, d), i.config.chart.dropShadow.enabled) { var y = i.config.chart.dropShadow; a.dropShadow(b, y, d) } this.addListeners(b, this.donutDataLabels), m.setAttrs(b.node, { "data:angle": u, "data:startAngle": n, "data:strokeWidth": this.strokeWidth, "data:value": e[d] }); var w = { x: 0, y: 0 }; "pie" === this.chartType || "polarArea" === this.chartType ? w = x.polarToCartesian(this.centerX, this.centerY, i.globals.radialSize / 1.25 + i.config.plotOptions.pie.dataLabels.offset, (n + u / 2) % this.fullAngle) : "donut" === this.chartType && (w = x.polarToCartesian(this.centerX, this.centerY, (i.globals.radialSize + this.donutSize) / 2 + i.config.plotOptions.pie.dataLabels.offset, (n + u / 2) % this.fullAngle)), g.add(b); var k = 0; if (!this.initialAnim || i.globals.resized || i.globals.dataChanged ? this.animBeginArr.push(0) : (0 === (k = u / this.fullAngle * i.config.chart.animations.speed) && (k = 1), this.animDur = k + this.animDur, this.animBeginArr.push(this.animDur)), this.dynamicAnim && i.globals.dataChanged ? this.animatePaths(b, { size: this.sliceSizes[d], endAngle: h, startAngle: n, prevStartAngle: l, prevEndAngle: c, animateStartingPos: !0, i: d, animBeginArr: this.animBeginArr, shouldSetPrevPaths: !0, dur: i.config.chart.animations.dynamicAnimation.speed }) : this.animatePaths(b, { size: this.sliceSizes[d], endAngle: h, startAngle: n, i: d, totalItems: t.length - 1, animBeginArr: this.animBeginArr, dur: k }), i.config.plotOptions.pie.expandOnClick && "polarArea" !== this.chartType && b.node.addEventListener("mouseup", this.pieClicked.bind(this, d)), void 0 !== i.globals.selectedDataPoints[0] && i.globals.selectedDataPoints[0].indexOf(d) > -1 && this.pieClicked(d), i.config.dataLabels.enabled) { var A = w.x, S = w.y, C = 100 * u / this.fullAngle + "%"; if (0 !== u && i.config.plotOptions.pie.dataLabels.minAngleToShowLabel < t[d]) { var L = i.config.dataLabels.formatter; void 0 !== L && (C = L(i.globals.seriesPercent[d][0], { seriesIndex: d, w: i })); var P = i.globals.dataLabels.style.colors[d], M = s.group({ class: "apexcharts-datalabels" }), I = s.drawText({ x: A, y: S, text: C, textAnchor: "middle", fontSize: i.config.dataLabels.style.fontSize, fontFamily: i.config.dataLabels.style.fontFamily, fontWeight: i.config.dataLabels.style.fontWeight, foreColor: P }); if (M.add(I), i.config.dataLabels.dropShadow.enabled) { var T = i.config.dataLabels.dropShadow; a.dropShadow(I, T) } I.node.classList.add("apexcharts-pie-label"), i.config.chart.animations.animate && !1 === i.globals.resized && (I.node.classList.add("apexcharts-pie-label-delay"), I.node.style.animationDelay = i.config.chart.animations.speed / 940 + "s"), this.sliceLabels.push(M) } } } return o } }, { key: "addListeners", value: function (t, e) { var i = new m(this.ctx); t.node.addEventListener("mouseenter", i.pathMouseEnter.bind(this, t)), t.node.addEventListener("mouseleave", i.pathMouseLeave.bind(this, t)), t.node.addEventListener("mouseleave", this.revertDataLabelsInner.bind(this, t.node, e)), t.node.addEventListener("mousedown", i.pathMouseDown.bind(this, t)), this.donutDataLabels.total.showAlways || (t.node.addEventListener("mouseenter", this.printDataLabelsInner.bind(this, t.node, e)), t.node.addEventListener("mousedown", this.printDataLabelsInner.bind(this, t.node, e))) } }, { key: "animatePaths", value: function (t, e) { var i = this.w, a = e.endAngle < e.startAngle ? this.fullAngle + e.endAngle - e.startAngle : e.endAngle - e.startAngle, s = a, r = e.startAngle, o = e.startAngle; void 0 !== e.prevStartAngle && void 0 !== e.prevEndAngle && (r = e.prevEndAngle, s = e.prevEndAngle < e.prevStartAngle ? this.fullAngle + e.prevEndAngle - e.prevStartAngle : e.prevEndAngle - e.prevStartAngle), e.i === i.config.series.length - 1 && (a + o > this.fullAngle ? e.endAngle = e.endAngle - (a + o) : a + o < this.fullAngle && (e.endAngle = e.endAngle + (this.fullAngle - (a + o)))), a === this.fullAngle && (a = this.fullAngle - .01), this.animateArc(t, r, o, a, s, e) } }, { key: "animateArc", value: function (t, e, i, a, s, r) { var o, n = this, l = this.w, h = new b(this.ctx), c = r.size; (isNaN(e) || isNaN(s)) && (e = i, s = a, r.dur = 0); var d = a, g = i, u = e < i ? this.fullAngle + e - i : e - i; l.globals.dataChanged && r.shouldSetPrevPaths && r.prevEndAngle && (o = n.getPiePath({ me: n, startAngle: r.prevStartAngle, angle: r.prevEndAngle < r.prevStartAngle ? this.fullAngle + r.prevEndAngle - r.prevStartAngle : r.prevEndAngle - r.prevStartAngle, size: c }), t.attr({ d: o })), 0 !== r.dur ? t.animate(r.dur, l.globals.easing, r.animBeginArr[r.i]).afterAll((function () { "pie" !== n.chartType && "donut" !== n.chartType && "polarArea" !== n.chartType || this.animate(l.config.chart.animations.dynamicAnimation.speed).attr({ "stroke-width": n.strokeWidth }), r.i === l.config.series.length - 1 && h.animationCompleted(t) })).during((function (l) { d = u + (a - u) * l, r.animateStartingPos && (d = s + (a - s) * l, g = e - s + (i - (e - s)) * l), o = n.getPiePath({ me: n, startAngle: g, angle: d, size: c }), t.node.setAttribute("data:pathOrig", o), t.attr({ d: o }) })) : (o = n.getPiePath({ me: n, startAngle: g, angle: a, size: c }), r.isTrack || (l.globals.animationEnded = !0), t.node.setAttribute("data:pathOrig", o), t.attr({ d: o, "stroke-width": n.strokeWidth })) } }, { key: "pieClicked", value: function (t) { var e, i = this.w, a = this, s = a.sliceSizes[t] + (i.config.plotOptions.pie.expandOnClick ? 4 : 0), r = i.globals.dom.Paper.select(".apexcharts-".concat(a.chartType.toLowerCase(), "-slice-").concat(t)).members[0]; if ("true" !== r.attr("data:pieClicked")) { var o = i.globals.dom.baseEl.getElementsByClassName("apexcharts-pie-area"); Array.prototype.forEach.call(o, (function (t) { t.setAttribute("data:pieClicked", "false"); var e = t.getAttribute("data:pathOrig"); e && t.setAttribute("d", e) })), i.globals.capturedDataPointIndex = t, r.attr("data:pieClicked", "true"); var n = parseInt(r.attr("data:startAngle"), 10), l = parseInt(r.attr("data:angle"), 10); e = a.getPiePath({ me: a, startAngle: n, angle: l, size: s }), 360 !== l && r.plot(e) } else { r.attr({ "data:pieClicked": "false" }), this.revertDataLabelsInner(r.node, this.donutDataLabels); var h = r.attr("data:pathOrig"); r.attr({ d: h }) } } }, { key: "getChangedPath", value: function (t, e) { var i = ""; return this.dynamicAnim && this.w.globals.dataChanged && (i = this.getPiePath({ me: this, startAngle: t, angle: e - t, size: this.size })), i } }, { key: "getPiePath", value: function (t) { var e, i = t.me, a = t.startAngle, s = t.angle, r = t.size, o = new m(this.ctx), n = a, l = Math.PI * (n - 90) / 180, h = s + a; Math.ceil(h) >= this.fullAngle + this.w.config.plotOptions.pie.startAngle % this.fullAngle && (h = this.fullAngle + this.w.config.plotOptions.pie.startAngle % this.fullAngle - .01), Math.ceil(h) > this.fullAngle && (h -= this.fullAngle); var c = Math.PI * (h - 90) / 180, d = i.centerX + r * Math.cos(l), g = i.centerY + r * Math.sin(l), u = i.centerX + r * Math.cos(c), p = i.centerY + r * Math.sin(c), f = x.polarToCartesian(i.centerX, i.centerY, i.donutSize, h), b = x.polarToCartesian(i.centerX, i.centerY, i.donutSize, n), v = s > 180 ? 1 : 0, y = ["M", d, g, "A", r, r, 0, v, 1, u, p]; return e = "donut" === i.chartType ? [].concat(y, ["L", f.x, f.y, "A", i.donutSize, i.donutSize, 0, v, 0, b.x, b.y, "L", d, g, "z"]).join(" ") : "pie" === i.chartType || "polarArea" === i.chartType ? [].concat(y, ["L", i.centerX, i.centerY, "L", d, g]).join(" ") : [].concat(y).join(" "), o.roundPathCorners(e, 2 * this.strokeWidth) } }, { key: "drawPolarElements", value: function (t) { var e = this.w, i = new _(this.ctx), a = new m(this.ctx), s = new Ct(this.ctx), r = a.group(), o = a.group(), n = i.niceScale(0, Math.ceil(this.maxY), 0), l = n.result.reverse(), h = n.result.length; this.maxY = n.niceMax; for (var c = e.globals.radialSize, d = c / (h - 1), g = 0; g < h - 1; g++) { var u = a.drawCircle(c); if (u.attr({ cx: this.centerX, cy: this.centerY, fill: "none", "stroke-width": e.config.plotOptions.polarArea.rings.strokeWidth, stroke: e.config.plotOptions.polarArea.rings.strokeColor }), e.config.yaxis[0].show) { var p = s.drawYAxisTexts(this.centerX, this.centerY - c + parseInt(e.config.yaxis[0].labels.style.fontSize, 10) / 2, g, l[g]); o.add(p) } r.add(u), c -= d } this.drawSpokes(t), t.add(r), t.add(o) } }, { key: "renderInnerDataLabels", value: function (t, e) { var i = this.w, a = new m(this.ctx), s = a.group({ class: "apexcharts-datalabels-group", transform: "translate(".concat(e.translateX ? e.translateX : 0, ", ").concat(e.translateY ? e.translateY : 0, ") scale(").concat(i.config.plotOptions.pie.customScale, ")") }), r = t.total.show; s.node.style.opacity = e.opacity; var o, n, l = e.centerX, h = e.centerY; o = void 0 === t.name.color ? i.globals.colors[0] : t.name.color; var c = t.name.fontSize, d = t.name.fontFamily, g = t.name.fontWeight; n = void 0 === t.value.color ? i.config.chart.foreColor : t.value.color; var u = t.value.formatter, p = "", f = ""; if (r ? (o = t.total.color, c = t.total.fontSize, d = t.total.fontFamily, g = t.total.fontWeight, f = t.total.label, p = t.total.formatter(i)) : 1 === i.globals.series.length && (p = u(i.globals.series[0], i), f = i.globals.seriesNames[0]), f && (f = t.name.formatter(f, t.total.show, i)), t.name.show) { var x = a.drawText({ x: l, y: h + parseFloat(t.name.offsetY), text: f, textAnchor: "middle", foreColor: o, fontSize: c, fontWeight: g, fontFamily: d }); x.node.classList.add("apexcharts-datalabel-label"), s.add(x) } if (t.value.show) { var b = t.name.show ? parseFloat(t.value.offsetY) + 16 : t.value.offsetY, v = a.drawText({ x: l, y: h + b, text: p, textAnchor: "middle", foreColor: n, fontWeight: t.value.fontWeight, fontSize: t.value.fontSize, fontFamily: t.value.fontFamily }); v.node.classList.add("apexcharts-datalabel-value"), s.add(v) } return s } }, { key: "printInnerLabels", value: function (t, e, i, a) { var s, r = this.w; a ? s = void 0 === t.name.color ? r.globals.colors[parseInt(a.parentNode.getAttribute("rel"), 10) - 1] : t.name.color : r.globals.series.length > 1 && t.total.show && (s = t.total.color); var o = r.globals.dom.baseEl.querySelector(".apexcharts-datalabel-label"), n = r.globals.dom.baseEl.querySelector(".apexcharts-datalabel-value"); i = (0, t.value.formatter)(i, r), a || "function" != typeof t.total.formatter || (i = t.total.formatter(r)); var l = e === t.total.label; e = t.name.formatter(e, l, r), null !== o && (o.textContent = e), null !== n && (n.textContent = i), null !== o && (o.style.fill = s) } }, { key: "printDataLabelsInner", value: function (t, e) { var i = this.w, a = t.getAttribute("data:value"), s = i.globals.seriesNames[parseInt(t.parentNode.getAttribute("rel"), 10) - 1]; i.globals.series.length > 1 && this.printInnerLabels(e, s, a, t); var r = i.globals.dom.baseEl.querySelector(".apexcharts-datalabels-group"); null !== r && (r.style.opacity = 1) } }, { key: "drawSpokes", value: function (t) { var e = this, i = this.w, a = new m(this.ctx), s = i.config.plotOptions.polarArea.spokes; if (0 !== s.strokeWidth) { for (var r = [], o = 360 / i.globals.series.length, n = 0; n < i.globals.series.length; n++)r.push(x.polarToCartesian(this.centerX, this.centerY, i.globals.radialSize, i.config.plotOptions.pie.startAngle + o * n)); r.forEach((function (i, r) { var o = a.drawLine(i.x, i.y, e.centerX, e.centerY, Array.isArray(s.connectorColors) ? s.connectorColors[r] : s.connectorColors); t.add(o) })) } } }, { key: "revertDataLabelsInner", value: function (t, e, i) { var a = this, s = this.w, r = s.globals.dom.baseEl.querySelector(".apexcharts-datalabels-group"), o = !1, n = s.globals.dom.baseEl.getElementsByClassName("apexcharts-pie-area"), l = function (t) { var i = t.makeSliceOut, s = t.printLabel; Array.prototype.forEach.call(n, (function (t) { "true" === t.getAttribute("data:pieClicked") && (i && (o = !0), s && a.printDataLabelsInner(t, e)) })) }; if (l({ makeSliceOut: !0, printLabel: !1 }), e.total.show && s.globals.series.length > 1) o && !e.total.showAlways ? l({ makeSliceOut: !1, printLabel: !0 }) : this.printInnerLabels(e, e.total.label, e.total.formatter(s)); else if (l({ makeSliceOut: !1, printLabel: !0 }), !o) if (s.globals.selectedDataPoints.length && s.globals.series.length > 1) if (s.globals.selectedDataPoints[0].length > 0) { var h = s.globals.selectedDataPoints[0], c = s.globals.dom.baseEl.querySelector(".apexcharts-".concat(this.chartType.toLowerCase(), "-slice-").concat(h)); this.printDataLabelsInner(c, e) } else r && s.globals.selectedDataPoints.length && 0 === s.globals.selectedDataPoints[0].length && (r.style.opacity = 0); else r && s.globals.series.length > 1 && (r.style.opacity = 0) } }]), t }(), Pt = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w, this.chartType = this.w.config.chart.type, this.initialAnim = this.w.config.chart.animations.enabled, this.dynamicAnim = this.initialAnim && this.w.config.chart.animations.dynamicAnimation.enabled, this.animDur = 0; var i = this.w; this.graphics = new m(this.ctx), this.lineColorArr = void 0 !== i.globals.stroke.colors ? i.globals.stroke.colors : i.globals.colors, this.defaultSize = i.globals.svgHeight < i.globals.svgWidth ? i.globals.gridHeight + 1.5 * i.globals.goldenPadding : i.globals.gridWidth, this.isLog = i.config.yaxis[0].logarithmic, this.logBase = i.config.yaxis[0].logBase, this.coreUtils = new y(this.ctx), this.maxValue = this.isLog ? this.coreUtils.getLogVal(this.logBase, i.globals.maxY, 0) : i.globals.maxY, this.minValue = this.isLog ? this.coreUtils.getLogVal(this.logBase, this.w.globals.minY, 0) : i.globals.minY, this.polygons = i.config.plotOptions.radar.polygons, this.strokeWidth = i.config.stroke.show ? i.config.stroke.width : 0, this.size = this.defaultSize / 2.1 - this.strokeWidth - i.config.chart.dropShadow.blur, i.config.xaxis.labels.show && (this.size = this.size - i.globals.xAxisLabelsWidth / 1.75), void 0 !== i.config.plotOptions.radar.size && (this.size = i.config.plotOptions.radar.size), this.dataRadiusOfPercent = [], this.dataRadius = [], this.angleArr = [], this.yaxisLabelsTextsPos = [] } return r(t, [{ key: "draw", value: function (t) { var i = this, a = this.w, s = new H(this.ctx), r = [], o = new N(this.ctx); t.length && (this.dataPointsLen = t[a.globals.maxValsInArrayIndex].length), this.disAngle = 2 * Math.PI / this.dataPointsLen; var n = a.globals.gridWidth / 2, l = a.globals.gridHeight / 2, h = n + a.config.plotOptions.radar.offsetX, c = l + a.config.plotOptions.radar.offsetY, d = this.graphics.group({ class: "apexcharts-radar-series apexcharts-plot-series", transform: "translate(".concat(h || 0, ", ").concat(c || 0, ")") }), g = [], u = null, p = null; if (this.yaxisLabels = this.graphics.group({ class: "apexcharts-yaxis" }), t.forEach((function (t, n) { var l = t.length === a.globals.dataPoints, h = i.graphics.group().attr({ class: "apexcharts-series", "data:longestSeries": l, seriesName: x.escapeString(a.globals.seriesNames[n]), rel: n + 1, "data:realIndex": n }); i.dataRadiusOfPercent[n] = [], i.dataRadius[n] = [], i.angleArr[n] = [], t.forEach((function (t, e) { var a = Math.abs(i.maxValue - i.minValue); t -= i.minValue, i.isLog && (t = i.coreUtils.getLogVal(i.logBase, t, 0)), i.dataRadiusOfPercent[n][e] = t / a, i.dataRadius[n][e] = i.dataRadiusOfPercent[n][e] * i.size, i.angleArr[n][e] = e * i.disAngle })), g = i.getDataPointsPos(i.dataRadius[n], i.angleArr[n]); var c = i.createPaths(g, { x: 0, y: 0 }); u = i.graphics.group({ class: "apexcharts-series-markers-wrap apexcharts-element-hidden" }), p = i.graphics.group({ class: "apexcharts-datalabels", "data:realIndex": n }), a.globals.delayedElements.push({ el: u.node, index: n }); var d = { i: n, realIndex: n, animationDelay: n, initialSpeed: a.config.chart.animations.speed, dataChangeSpeed: a.config.chart.animations.dynamicAnimation.speed, className: "apexcharts-radar", shouldClipToGrid: !1, bindEventsOnPaths: !1, stroke: a.globals.stroke.colors[n], strokeLineCap: a.config.stroke.lineCap }, f = null; a.globals.previousPaths.length > 0 && (f = i.getPreviousPath(n)); for (var b = 0; b < c.linePathsTo.length; b++) { var m = i.graphics.renderPaths(e(e({}, d), {}, { pathFrom: null === f ? c.linePathsFrom[b] : f, pathTo: c.linePathsTo[b], strokeWidth: Array.isArray(i.strokeWidth) ? i.strokeWidth[n] : i.strokeWidth, fill: "none", drawShadow: !1 })); h.add(m); var y = s.fillPath({ seriesNumber: n }), w = i.graphics.renderPaths(e(e({}, d), {}, { pathFrom: null === f ? c.areaPathsFrom[b] : f, pathTo: c.areaPathsTo[b], strokeWidth: 0, fill: y, drawShadow: !1 })); if (a.config.chart.dropShadow.enabled) { var k = new v(i.ctx), A = a.config.chart.dropShadow; k.dropShadow(w, Object.assign({}, A, { noUserSpaceOnUse: !0 }), n) } h.add(w) } t.forEach((function (t, s) { var r = new D(i.ctx).getMarkerConfig({ cssClass: "apexcharts-marker", seriesIndex: n, dataPointIndex: s }), l = i.graphics.drawMarker(g[s].x, g[s].y, r); l.attr("rel", s), l.attr("j", s), l.attr("index", n), l.node.setAttribute("default-marker-size", r.pSize); var c = i.graphics.group({ class: "apexcharts-series-markers" }); c && c.add(l), u.add(c), h.add(u); var d = a.config.dataLabels; if (d.enabled) { var f = d.formatter(a.globals.series[n][s], { seriesIndex: n, dataPointIndex: s, w: a }); o.plotDataLabelsText({ x: g[s].x, y: g[s].y, text: f, textAnchor: "middle", i: n, j: n, parent: p, offsetCorrection: !1, dataLabelsConfig: e({}, d) }) } h.add(p) })), r.push(h) })), this.drawPolygons({ parent: d }), a.config.xaxis.labels.show) { var f = this.drawXAxisTexts(); d.add(f) } return r.forEach((function (t) { d.add(t) })), d.add(this.yaxisLabels), d } }, { key: "drawPolygons", value: function (t) { for (var e = this, i = this.w, a = t.parent, s = new Ct(this.ctx), r = i.globals.yAxisScale[0].result.reverse(), o = r.length, n = [], l = this.size / (o - 1), h = 0; h < o; h++)n[h] = l * h; n.reverse(); var c = [], d = []; n.forEach((function (t, i) { var a = x.getPolygonPos(t, e.dataPointsLen), s = ""; a.forEach((function (t, a) { if (0 === i) { var r = e.graphics.drawLine(t.x, t.y, 0, 0, Array.isArray(e.polygons.connectorColors) ? e.polygons.connectorColors[a] : e.polygons.connectorColors); d.push(r) } 0 === a && e.yaxisLabelsTextsPos.push({ x: t.x, y: t.y }), s += t.x + "," + t.y + " " })), c.push(s) })), c.forEach((function (t, s) { var r = e.polygons.strokeColors, o = e.polygons.strokeWidth, n = e.graphics.drawPolygon(t, Array.isArray(r) ? r[s] : r, Array.isArray(o) ? o[s] : o, i.globals.radarPolygons.fill.colors[s]); a.add(n) })), d.forEach((function (t) { a.add(t) })), i.config.yaxis[0].show && this.yaxisLabelsTextsPos.forEach((function (t, i) { var a = s.drawYAxisTexts(t.x, t.y, i, r[i]); e.yaxisLabels.add(a) })) } }, { key: "drawXAxisTexts", value: function () { var t = this, i = this.w, a = i.config.xaxis.labels, s = this.graphics.group({ class: "apexcharts-xaxis" }), r = x.getPolygonPos(this.size, this.dataPointsLen); return i.globals.labels.forEach((function (o, n) { var l = i.config.xaxis.labels.formatter, h = new N(t.ctx); if (r[n]) { var c = t.getTextPos(r[n], t.size), d = l(o, { seriesIndex: -1, dataPointIndex: n, w: i }); h.plotDataLabelsText({ x: c.newX, y: c.newY, text: d, textAnchor: c.textAnchor, i: n, j: n, parent: s, color: Array.isArray(a.style.colors) && a.style.colors[n] ? a.style.colors[n] : "#a8a8a8", dataLabelsConfig: e({ textAnchor: c.textAnchor, dropShadow: { enabled: !1 } }, a), offsetCorrection: !1 }) } })), s } }, { key: "createPaths", value: function (t, e) { var i = this, a = [], s = [], r = [], o = []; if (t.length) { s = [this.graphics.move(e.x, e.y)], o = [this.graphics.move(e.x, e.y)]; var n = this.graphics.move(t[0].x, t[0].y), l = this.graphics.move(t[0].x, t[0].y); t.forEach((function (e, a) { n += i.graphics.line(e.x, e.y), l += i.graphics.line(e.x, e.y), a === t.length - 1 && (n += "Z", l += "Z") })), a.push(n), r.push(l) } return { linePathsFrom: s, linePathsTo: a, areaPathsFrom: o, areaPathsTo: r } } }, { key: "getTextPos", value: function (t, e) { var i = "middle", a = t.x, s = t.y; return Math.abs(t.x) >= 10 ? t.x > 0 ? (i = "start", a += 10) : t.x < 0 && (i = "end", a -= 10) : i = "middle", Math.abs(t.y) >= e - 10 && (t.y < 0 ? s -= 10 : t.y > 0 && (s += 10)), { textAnchor: i, newX: a, newY: s } } }, { key: "getPreviousPath", value: function (t) { for (var e = this.w, i = null, a = 0; a < e.globals.previousPaths.length; a++) { var s = e.globals.previousPaths[a]; s.paths.length > 0 && parseInt(s.realIndex, 10) === parseInt(t, 10) && void 0 !== e.globals.previousPaths[a].paths[0] && (i = e.globals.previousPaths[a].paths[0].d) } return i } }, { key: "getDataPointsPos", value: function (t, e) { var i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : this.dataPointsLen; t = t || [], e = e || []; for (var a = [], s = 0; s < i; s++) { var r = {}; r.x = t[s] * Math.sin(e[s]), r.y = -t[s] * Math.cos(e[s]), a.push(r) } return a } }]), t }(), Mt = function (t) { n(i, t); var e = d(i); function i(t) { var s; a(this, i), (s = e.call(this, t)).ctx = t, s.w = t.w, s.animBeginArr = [0], s.animDur = 0; var r = s.w; return s.startAngle = r.config.plotOptions.radialBar.startAngle, s.endAngle = r.config.plotOptions.radialBar.endAngle, s.totalAngle = Math.abs(r.config.plotOptions.radialBar.endAngle - r.config.plotOptions.radialBar.startAngle), s.trackStartAngle = r.config.plotOptions.radialBar.track.startAngle, s.trackEndAngle = r.config.plotOptions.radialBar.track.endAngle, s.barLabels = s.w.config.plotOptions.radialBar.barLabels, s.donutDataLabels = s.w.config.plotOptions.radialBar.dataLabels, s.radialDataLabels = s.donutDataLabels, s.trackStartAngle || (s.trackStartAngle = s.startAngle), s.trackEndAngle || (s.trackEndAngle = s.endAngle), 360 === s.endAngle && (s.endAngle = 359.99), s.margin = parseInt(r.config.plotOptions.radialBar.track.margin, 10), s.onBarLabelClick = s.onBarLabelClick.bind(c(s)), s } return r(i, [{ key: "draw", value: function (t) { var e = this.w, i = new m(this.ctx), a = i.group({ class: "apexcharts-radialbar" }); if (e.globals.noData) return a; var s = i.group(), r = this.defaultSize / 2, o = e.globals.gridWidth / 2, n = this.defaultSize / 2.05; e.config.chart.sparkline.enabled || (n = n - e.config.stroke.width - e.config.chart.dropShadow.blur); var l = e.globals.fill.colors; if (e.config.plotOptions.radialBar.track.show) { var h = this.drawTracks({ size: n, centerX: o, centerY: r, colorArr: l, series: t }); s.add(h) } var c = this.drawArcs({ size: n, centerX: o, centerY: r, colorArr: l, series: t }), d = 360; e.config.plotOptions.radialBar.startAngle < 0 && (d = this.totalAngle); var g = (360 - d) / 360; if (e.globals.radialSize = n - n * g, this.radialDataLabels.value.show) { var u = Math.max(this.radialDataLabels.value.offsetY, this.radialDataLabels.name.offsetY); e.globals.radialSize += u * g } return s.add(c.g), "front" === e.config.plotOptions.radialBar.hollow.position && (c.g.add(c.elHollow), c.dataLabels && c.g.add(c.dataLabels)), a.add(s), a } }, { key: "drawTracks", value: function (t) { var e = this.w, i = new m(this.ctx), a = i.group({ class: "apexcharts-tracks" }), s = new v(this.ctx), r = new H(this.ctx), o = this.getStrokeWidth(t); t.size = t.size - o / 2; for (var n = 0; n < t.series.length; n++) { var l = i.group({ class: "apexcharts-radialbar-track apexcharts-track" }); a.add(l), l.attr({ rel: n + 1 }), t.size = t.size - o - this.margin; var h = e.config.plotOptions.radialBar.track, c = r.fillPath({ seriesNumber: 0, size: t.size, fillColors: Array.isArray(h.background) ? h.background[n] : h.background, solid: !0 }), d = this.trackStartAngle, g = this.trackEndAngle; Math.abs(g) + Math.abs(d) >= 360 && (g = 360 - Math.abs(this.startAngle) - .1); var u = i.drawPath({ d: "", stroke: c, strokeWidth: o * parseInt(h.strokeWidth, 10) / 100, fill: "none", strokeOpacity: h.opacity, classes: "apexcharts-radialbar-area" }); if (h.dropShadow.enabled) { var p = h.dropShadow; s.dropShadow(u, p) } l.add(u), u.attr("id", "apexcharts-radialbarTrack-" + n), this.animatePaths(u, { centerX: t.centerX, centerY: t.centerY, endAngle: g, startAngle: d, size: t.size, i: n, totalItems: 2, animBeginArr: 0, dur: 0, isTrack: !0, easing: e.globals.easing }) } return a } }, { key: "drawArcs", value: function (t) { var e = this.w, i = new m(this.ctx), a = new H(this.ctx), s = new v(this.ctx), r = i.group(), o = this.getStrokeWidth(t); t.size = t.size - o / 2; var n = e.config.plotOptions.radialBar.hollow.background, l = t.size - o * t.series.length - this.margin * t.series.length - o * parseInt(e.config.plotOptions.radialBar.track.strokeWidth, 10) / 100 / 2, h = l - e.config.plotOptions.radialBar.hollow.margin; void 0 !== e.config.plotOptions.radialBar.hollow.image && (n = this.drawHollowImage(t, r, l, n)); var c = this.drawHollow({ size: h, centerX: t.centerX, centerY: t.centerY, fill: n || "transparent" }); if (e.config.plotOptions.radialBar.hollow.dropShadow.enabled) { var d = e.config.plotOptions.radialBar.hollow.dropShadow; s.dropShadow(c, d) } var g = 1; !this.radialDataLabels.total.show && e.globals.series.length > 1 && (g = 0); var u = null; this.radialDataLabels.show && (u = this.renderInnerDataLabels(this.radialDataLabels, { hollowSize: l, centerX: t.centerX, centerY: t.centerY, opacity: g })), "back" === e.config.plotOptions.radialBar.hollow.position && (r.add(c), u && r.add(u)); var p = !1; e.config.plotOptions.radialBar.inverseOrder && (p = !0); for (var f = p ? t.series.length - 1 : 0; p ? f >= 0 : f < t.series.length; p ? f-- : f++) { var b = i.group({ class: "apexcharts-series apexcharts-radial-series", seriesName: x.escapeString(e.globals.seriesNames[f]) }); r.add(b), b.attr({ rel: f + 1, "data:realIndex": f }), this.ctx.series.addCollapsedClassToSeries(b, f), t.size = t.size - o - this.margin; var y = a.fillPath({ seriesNumber: f, size: t.size, value: t.series[f] }), w = this.startAngle, k = void 0, A = x.negToZero(t.series[f] > 100 ? 100 : t.series[f]) / 100, S = Math.round(this.totalAngle * A) + this.startAngle, C = void 0; e.globals.dataChanged && (k = this.startAngle, C = Math.round(this.totalAngle * x.negToZero(e.globals.previousPaths[f]) / 100) + k), Math.abs(S) + Math.abs(w) >= 360 && (S -= .01), Math.abs(C) + Math.abs(k) >= 360 && (C -= .01); var L = S - w, P = Array.isArray(e.config.stroke.dashArray) ? e.config.stroke.dashArray[f] : e.config.stroke.dashArray, M = i.drawPath({ d: "", stroke: y, strokeWidth: o, fill: "none", fillOpacity: e.config.fill.opacity, classes: "apexcharts-radialbar-area apexcharts-radialbar-slice-" + f, strokeDashArray: P }); if (m.setAttrs(M.node, { "data:angle": L, "data:value": t.series[f] }), e.config.chart.dropShadow.enabled) { var I = e.config.chart.dropShadow; s.dropShadow(M, I, f) } if (s.setSelectionFilter(M, 0, f), this.addListeners(M, this.radialDataLabels), b.add(M), M.attr({ index: 0, j: f }), this.barLabels.enabled) { var T = x.polarToCartesian(t.centerX, t.centerY, t.size, w), z = this.barLabels.formatter(e.globals.seriesNames[f], { seriesIndex: f, w: e }), X = ["apexcharts-radialbar-label"]; this.barLabels.onClick || X.push("apexcharts-no-click"); var E = this.barLabels.useSeriesColors ? e.globals.colors[f] : e.config.chart.foreColor; E || (E = e.config.chart.foreColor); var Y = T.x - this.barLabels.margin, F = T.y, R = i.drawText({ x: Y, y: F, text: z, textAnchor: "end", dominantBaseline: "middle", fontFamily: this.barLabels.fontFamily, fontWeight: this.barLabels.fontWeight, fontSize: this.barLabels.fontSize, foreColor: E, cssClass: X.join(" ") }); R.on("click", this.onBarLabelClick), R.attr({ rel: f + 1 }), 0 !== w && R.attr({ "transform-origin": "".concat(Y, " ").concat(F), transform: "rotate(".concat(w, " 0 0)") }), b.add(R) } var D = 0; !this.initialAnim || e.globals.resized || e.globals.dataChanged || (D = e.config.chart.animations.speed), e.globals.dataChanged && (D = e.config.chart.animations.dynamicAnimation.speed), this.animDur = D / (1.2 * t.series.length) + this.animDur, this.animBeginArr.push(this.animDur), this.animatePaths(M, { centerX: t.centerX, centerY: t.centerY, endAngle: S, startAngle: w, prevEndAngle: C, prevStartAngle: k, size: t.size, i: f, totalItems: 2, animBeginArr: this.animBeginArr, dur: D, shouldSetPrevPaths: !0, easing: e.globals.easing }) } return { g: r, elHollow: c, dataLabels: u } } }, { key: "drawHollow", value: function (t) { var e = new m(this.ctx).drawCircle(2 * t.size); return e.attr({ class: "apexcharts-radialbar-hollow", cx: t.centerX, cy: t.centerY, r: t.size, fill: t.fill }), e } }, { key: "drawHollowImage", value: function (t, e, i, a) { var s = this.w, r = new H(this.ctx), o = x.randomId(), n = s.config.plotOptions.radialBar.hollow.image; if (s.config.plotOptions.radialBar.hollow.imageClipped) r.clippedImgArea({ width: i, height: i, image: n, patternID: "pattern".concat(s.globals.cuid).concat(o) }), a = "url(#pattern".concat(s.globals.cuid).concat(o, ")"); else { var l = s.config.plotOptions.radialBar.hollow.imageWidth, h = s.config.plotOptions.radialBar.hollow.imageHeight; if (void 0 === l && void 0 === h) { var c = s.globals.dom.Paper.image(n).loaded((function (e) { this.move(t.centerX - e.width / 2 + s.config.plotOptions.radialBar.hollow.imageOffsetX, t.centerY - e.height / 2 + s.config.plotOptions.radialBar.hollow.imageOffsetY) })); e.add(c) } else { var d = s.globals.dom.Paper.image(n).loaded((function (e) { this.move(t.centerX - l / 2 + s.config.plotOptions.radialBar.hollow.imageOffsetX, t.centerY - h / 2 + s.config.plotOptions.radialBar.hollow.imageOffsetY), this.size(l, h) })); e.add(d) } } return a } }, { key: "getStrokeWidth", value: function (t) { var e = this.w; return t.size * (100 - parseInt(e.config.plotOptions.radialBar.hollow.size, 10)) / 100 / (t.series.length + 1) - this.margin } }, { key: "onBarLabelClick", value: function (t) { var e = parseInt(t.target.getAttribute("rel"), 10) - 1, i = this.barLabels.onClick, a = this.w; i && i(a.globals.seriesNames[e], { w: a, seriesIndex: e }) } }]), i }(Lt), It = function (t) { n(s, t); var i = d(s); function s() { return a(this, s), i.apply(this, arguments) } return r(s, [{ key: "draw", value: function (t, i) { var a = this.w, s = new m(this.ctx); this.rangeBarOptions = this.w.config.plotOptions.rangeBar, this.series = t, this.seriesRangeStart = a.globals.seriesRangeStart, this.seriesRangeEnd = a.globals.seriesRangeEnd, this.barHelpers.initVariables(t); for (var r = s.group({ class: "apexcharts-rangebar-series apexcharts-plot-series" }), n = 0; n < t.length; n++) { var l, h, c, d, g = void 0, u = void 0, p = a.globals.comboCharts ? i[n] : n, f = this.barHelpers.getGroupIndex(p).columnGroupIndex, b = s.group({ class: "apexcharts-series", seriesName: x.escapeString(a.globals.seriesNames[p]), rel: n + 1, "data:realIndex": p }); this.ctx.series.addCollapsedClassToSeries(b, p), t[n].length > 0 && (this.visibleI = this.visibleI + 1); var v = 0, y = 0, w = 0; this.yRatio.length > 1 && (this.yaxisIndex = a.globals.seriesYAxisReverseMap[p][0], w = p); var k = this.barHelpers.initialPositions(); u = k.y, d = k.zeroW, g = k.x, y = k.barWidth, v = k.barHeight, l = k.xDivision, h = k.yDivision, c = k.zeroH; for (var A = s.group({ class: "apexcharts-datalabels", "data:realIndex": p }), S = s.group({ class: "apexcharts-rangebar-goals-markers" }), C = 0; C < a.globals.dataPoints; C++) { var L, P = this.barHelpers.getStrokeWidth(n, C, p), M = this.seriesRangeStart[n][C], I = this.seriesRangeEnd[n][C], T = null, z = null, X = null, E = { x: g, y: u, strokeWidth: P, elSeries: b }, Y = this.seriesLen; if (a.config.plotOptions.bar.rangeBarGroupRows && (Y = 1), void 0 === a.config.series[n].data[C]) break; if (this.isHorizontal) { X = u + v * this.visibleI; var F = (h - v * Y) / 2; if (a.config.series[n].data[C].x) { var R = this.detectOverlappingBars({ i: n, j: C, barYPosition: X, srty: F, barHeight: v, yDivision: h, initPositions: k }); v = R.barHeight, X = R.barYPosition } y = (T = this.drawRangeBarPaths(e({ indexes: { i: n, j: C, realIndex: p }, barHeight: v, barYPosition: X, zeroW: d, yDivision: h, y1: M, y2: I }, E))).barWidth } else { a.globals.isXNumeric && (g = (a.globals.seriesX[n][C] - a.globals.minX) / this.xRatio - y / 2), z = g + y * this.visibleI; var H = (l - y * Y) / 2; if (a.config.series[n].data[C].x) { var D = this.detectOverlappingBars({ i: n, j: C, barXPosition: z, srtx: H, barWidth: y, xDivision: l, initPositions: k }); y = D.barWidth, z = D.barXPosition } v = (T = this.drawRangeColumnPaths(e({ indexes: { i: n, j: C, realIndex: p, translationsIndex: w }, barWidth: y, barXPosition: z, zeroH: c, xDivision: l }, E))).barHeight } var O = this.barHelpers.drawGoalLine({ barXPosition: T.barXPosition, barYPosition: X, goalX: T.goalX, goalY: T.goalY, barHeight: v, barWidth: y }); O && S.add(O), u = T.y, g = T.x; var N = this.barHelpers.getPathFillColor(t, n, C, p), W = a.globals.stroke.colors[p]; this.renderSeries((o(L = { realIndex: p, pathFill: N, lineFill: W, j: C, i: n, x: g, y: u, y1: M, y2: I, pathFrom: T.pathFrom, pathTo: T.pathTo, strokeWidth: P, elSeries: b, series: t, barHeight: v, barWidth: y, barXPosition: z, barYPosition: X }, "barWidth", y), o(L, "columnGroupIndex", f), o(L, "elDataLabelsWrap", A), o(L, "elGoalsMarkers", S), o(L, "visibleSeries", this.visibleI), o(L, "type", "rangebar"), L)) } r.add(b) } return r } }, { key: "detectOverlappingBars", value: function (t) { var e = t.i, i = t.j, a = t.barYPosition, s = t.barXPosition, r = t.srty, o = t.srtx, n = t.barHeight, l = t.barWidth, h = t.yDivision, c = t.xDivision, d = t.initPositions, g = this.w, u = [], p = g.config.series[e].data[i].rangeName, f = g.config.series[e].data[i].x, x = Array.isArray(f) ? f.join(" ") : f, b = g.globals.labels.map((function (t) { return Array.isArray(t) ? t.join(" ") : t })).indexOf(x), v = g.globals.seriesRange[e].findIndex((function (t) { return t.x === x && t.overlaps.length > 0 })); return this.isHorizontal ? (a = g.config.plotOptions.bar.rangeBarGroupRows ? r + h * b : r + n * this.visibleI + h * b, v > -1 && !g.config.plotOptions.bar.rangeBarOverlap && (u = g.globals.seriesRange[e][v].overlaps).indexOf(p) > -1 && (a = (n = d.barHeight / u.length) * this.visibleI + h * (100 - parseInt(this.barOptions.barHeight, 10)) / 100 / 2 + n * (this.visibleI + u.indexOf(p)) + h * b)) : (b > -1 && !g.globals.timescaleLabels.length && (s = g.config.plotOptions.bar.rangeBarGroupRows ? o + c * b : o + l * this.visibleI + c * b), v > -1 && !g.config.plotOptions.bar.rangeBarOverlap && (u = g.globals.seriesRange[e][v].overlaps).indexOf(p) > -1 && (s = (l = d.barWidth / u.length) * this.visibleI + c * (100 - parseInt(this.barOptions.barWidth, 10)) / 100 / 2 + l * (this.visibleI + u.indexOf(p)) + c * b)), { barYPosition: a, barXPosition: s, barHeight: n, barWidth: l } } }, { key: "drawRangeColumnPaths", value: function (t) { var e = t.indexes, i = t.x, a = t.xDivision, s = t.barWidth, r = t.barXPosition, o = t.zeroH, n = this.w, l = e.i, h = e.j, c = e.realIndex, d = e.translationsIndex, g = this.yRatio[d], u = this.getRangeValue(c, h), p = Math.min(u.start, u.end), f = Math.max(u.start, u.end); void 0 === this.series[l][h] || null === this.series[l][h] ? p = o : (p = o - p / g, f = o - f / g); var x = Math.abs(f - p), b = this.barHelpers.getColumnPaths({ barXPosition: r, barWidth: s, y1: p, y2: f, strokeWidth: this.strokeWidth, series: this.seriesRangeEnd, realIndex: c, i: c, j: h, w: n }); if (n.globals.isXNumeric) { var v = this.getBarXForNumericXAxis({ x: i, j: h, realIndex: c, barWidth: s }); i = v.x, r = v.barXPosition } else i += a; return { pathTo: b.pathTo, pathFrom: b.pathFrom, barHeight: x, x: i, y: u.start < 0 && u.end < 0 ? p : f, goalY: this.barHelpers.getGoalValues("y", null, o, l, h, d), barXPosition: r } } }, { key: "drawRangeBarPaths", value: function (t) { var e = t.indexes, i = t.y, a = t.y1, s = t.y2, r = t.yDivision, o = t.barHeight, n = t.barYPosition, l = t.zeroW, h = this.w, c = e.realIndex, d = e.j, g = l + a / this.invertedYRatio, u = l + s / this.invertedYRatio, p = this.getRangeValue(c, d), f = Math.abs(u - g), x = this.barHelpers.getBarpaths({ barYPosition: n, barHeight: o, x1: g, x2: u, strokeWidth: this.strokeWidth, series: this.seriesRangeEnd, i: c, realIndex: c, j: d, w: h }); return h.globals.isXNumeric || (i += r), { pathTo: x.pathTo, pathFrom: x.pathFrom, barWidth: f, x: p.start < 0 && p.end < 0 ? g : u, goalX: this.barHelpers.getGoalValues("x", l, null, c, d), y: i } } }, { key: "getRangeValue", value: function (t, e) { var i = this.w; return { start: i.globals.seriesRangeStart[t][e], end: i.globals.seriesRangeEnd[t][e] } } }]), s }(yt), Tt = function () { function t(e) { a(this, t), this.w = e.w, this.lineCtx = e } return r(t, [{ key: "sameValueSeriesFix", value: function (t, e) { var i = this.w; if (("gradient" === i.config.fill.type || "gradient" === i.config.fill.type[t]) && new y(this.lineCtx.ctx, i).seriesHaveSameValues(t)) { var a = e[t].slice(); a[a.length - 1] = a[a.length - 1] + 1e-6, e[t] = a } return e } }, { key: "calculatePoints", value: function (t) { var e = t.series, i = t.realIndex, a = t.x, s = t.y, r = t.i, o = t.j, n = t.prevY, l = this.w, h = [], c = []; if (0 === o) { var d = this.lineCtx.categoryAxisCorrection + l.config.markers.offsetX; l.globals.isXNumeric && (d = (l.globals.seriesX[i][0] - l.globals.minX) / this.lineCtx.xRatio + l.config.markers.offsetX), h.push(d), c.push(x.isNumber(e[r][0]) ? n + l.config.markers.offsetY : null), h.push(a + l.config.markers.offsetX), c.push(x.isNumber(e[r][o + 1]) ? s + l.config.markers.offsetY : null) } else h.push(a + l.config.markers.offsetX), c.push(x.isNumber(e[r][o + 1]) ? s + l.config.markers.offsetY : null); return { x: h, y: c } } }, { key: "checkPreviousPaths", value: function (t) { for (var e = t.pathFromLine, i = t.pathFromArea, a = t.realIndex, s = this.w, r = 0; r < s.globals.previousPaths.length; r++) { var o = s.globals.previousPaths[r]; ("line" === o.type || "area" === o.type) && o.paths.length > 0 && parseInt(o.realIndex, 10) === parseInt(a, 10) && ("line" === o.type ? (this.lineCtx.appendPathFrom = !1, e = s.globals.previousPaths[r].paths[0].d) : "area" === o.type && (this.lineCtx.appendPathFrom = !1, i = s.globals.previousPaths[r].paths[0].d, s.config.stroke.show && s.globals.previousPaths[r].paths[1] && (e = s.globals.previousPaths[r].paths[1].d))) } return { pathFromLine: e, pathFromArea: i } } }, { key: "determineFirstPrevY", value: function (t) { var e, i, a, s = t.i, r = t.realIndex, o = t.series, n = t.prevY, l = t.lineYPosition, h = t.translationsIndex, c = this.w, d = c.config.chart.stacked && !c.globals.comboCharts || c.config.chart.stacked && c.globals.comboCharts && (!this.w.config.chart.stackOnlyBar || "bar" === (null === (e = this.w.config.series[r]) || void 0 === e ? void 0 : e.type) || "column" === (null === (i = this.w.config.series[r]) || void 0 === i ? void 0 : i.type)); if (void 0 !== (null === (a = o[s]) || void 0 === a ? void 0 : a[0])) n = (l = d && s > 0 ? this.lineCtx.prevSeriesY[s - 1][0] : this.lineCtx.zeroY) - o[s][0] / this.lineCtx.yRatio[h] + 2 * (this.lineCtx.isReversed ? o[s][0] / this.lineCtx.yRatio[h] : 0); else if (d && s > 0 && void 0 === o[s][0]) for (var g = s - 1; g >= 0; g--)if (null !== o[g][0] && void 0 !== o[g][0]) { n = l = this.lineCtx.prevSeriesY[g][0]; break } return { prevY: n, lineYPosition: l } } }]), t }(), zt = function (t) { for (var e, i, a, s, r = function (t) { for (var e = [], i = t[0], a = t[1], s = e[0] = Yt(i, a), r = 1, o = t.length - 1; r < o; r++)i = a, a = t[r + 1], e[r] = .5 * (s + (s = Yt(i, a))); return e[r] = s, e }(t), o = t.length - 1, n = [], l = 0; l < o; l++)a = Yt(t[l], t[l + 1]), Math.abs(a) < 1e-6 ? r[l] = r[l + 1] = 0 : (s = (e = r[l] / a) * e + (i = r[l + 1] / a) * i) > 9 && (s = 3 * a / Math.sqrt(s), r[l] = s * e, r[l + 1] = s * i); for (var h = 0; h <= o; h++)s = (t[Math.min(o, h + 1)][0] - t[Math.max(0, h - 1)][0]) / (6 * (1 + r[h] * r[h])), n.push([s || 0, r[h] * s || 0]); return n }, Xt = function (t) { var e = zt(t), i = t[1], a = t[0], s = [], r = e[1], o = e[0]; s.push(a, [a[0] + o[0], a[1] + o[1], i[0] - r[0], i[1] - r[1], i[0], i[1]]); for (var n = 2, l = e.length; n < l; n++) { var h = t[n], c = e[n]; s.push([h[0] - c[0], h[1] - c[1], h[0], h[1]]) } return s }, Et = function (t, e, i) { var a = t.slice(e, i); if (e) { if (i - e > 1 && a[1].length < 6) { var s = a[0].length; a[1] = [2 * a[0][s - 2] - a[0][s - 4], 2 * a[0][s - 1] - a[0][s - 3]].concat(a[1]) } a[0] = a[0].slice(-2) } return a }; function Yt(t, e) { return (e[1] - t[1]) / (e[0] - t[0]) } var Ft = function () { function t(e, i, s) { a(this, t), this.ctx = e, this.w = e.w, this.xyRatios = i, this.pointsChart = !("bubble" !== this.w.config.chart.type && "scatter" !== this.w.config.chart.type) || s, this.scatter = new O(this.ctx), this.noNegatives = this.w.globals.minX === Number.MAX_VALUE, this.lineHelpers = new Tt(this), this.markers = new D(this.ctx), this.prevSeriesY = [], this.categoryAxisCorrection = 0, this.yaxisIndex = 0 } return r(t, [{ key: "draw", value: function (t, i, a, s) { var r, o = this.w, n = new m(this.ctx), l = o.globals.comboCharts ? i : o.config.chart.type, h = n.group({ class: "apexcharts-".concat(l, "-series apexcharts-plot-series") }), c = new y(this.ctx, o); this.yRatio = this.xyRatios.yRatio, this.zRatio = this.xyRatios.zRatio, this.xRatio = this.xyRatios.xRatio, this.baseLineY = this.xyRatios.baseLineY, t = c.getLogSeries(t), this.yRatio = c.getLogYRatios(this.yRatio), this.prevSeriesY = []; for (var d = [], g = 0; g < t.length; g++) { t = this.lineHelpers.sameValueSeriesFix(g, t); var u = o.globals.comboCharts ? a[g] : g, p = this.yRatio.length > 1 ? u : 0; this._initSerieVariables(t, g, u); var f = [], x = [], b = [], v = o.globals.padHorizontal + this.categoryAxisCorrection; this.ctx.series.addCollapsedClassToSeries(this.elSeries, u), o.globals.isXNumeric && o.globals.seriesX.length > 0 && (v = (o.globals.seriesX[u][0] - o.globals.minX) / this.xRatio), b.push(v); var w, k = v, A = void 0, S = k, C = this.zeroY, L = this.zeroY; C = this.lineHelpers.determineFirstPrevY({ i: g, realIndex: u, series: t, prevY: C, lineYPosition: 0, translationsIndex: p }).prevY, "monotoneCubic" === o.config.stroke.curve && null === t[g][0] ? f.push(null) : f.push(C), w = C; "rangeArea" === l && (A = L = this.lineHelpers.determineFirstPrevY({ i: g, realIndex: u, series: s, prevY: L, lineYPosition: 0, translationsIndex: p }).prevY, x.push(null !== f[0] ? L : null)); var P = this._calculatePathsFrom({ type: l, series: t, i: g, realIndex: u, translationsIndex: p, prevX: S, prevY: C, prevY2: L }), M = [f[0]], I = [x[0]], T = { type: l, series: t, realIndex: u, translationsIndex: p, i: g, x: v, y: 1, pX: k, pY: w, pathsFrom: P, linePaths: [], areaPaths: [], seriesIndex: a, lineYPosition: 0, xArrj: b, yArrj: f, y2Arrj: x, seriesRangeEnd: s }, z = this._iterateOverDataPoints(e(e({}, T), {}, { iterations: "rangeArea" === l ? t[g].length - 1 : void 0, isRangeStart: !0 })); if ("rangeArea" === l) { for (var X = this._calculatePathsFrom({ series: s, i: g, realIndex: u, prevX: S, prevY: L }), E = this._iterateOverDataPoints(e(e({}, T), {}, { series: s, xArrj: [v], yArrj: M, y2Arrj: I, pY: A, areaPaths: z.areaPaths, pathsFrom: X, iterations: s[g].length - 1, isRangeStart: !1 })), Y = z.linePaths.length / 2, F = 0; F < Y; F++)z.linePaths[F] = E.linePaths[F + Y] + z.linePaths[F]; z.linePaths.splice(Y), z.pathFromLine = E.pathFromLine + z.pathFromLine } else z.pathFromArea += n.line(0, this.zeroY); this._handlePaths({ type: l, realIndex: u, i: g, paths: z }), this.elSeries.add(this.elPointsMain), this.elSeries.add(this.elDataLabelsWrap), d.push(this.elSeries) } if (void 0 !== (null === (r = o.config.series[0]) || void 0 === r ? void 0 : r.zIndex) && d.sort((function (t, e) { return Number(t.node.getAttribute("zIndex")) - Number(e.node.getAttribute("zIndex")) })), o.config.chart.stacked) for (var R = d.length - 1; R >= 0; R--)h.add(d[R]); else for (var H = 0; H < d.length; H++)h.add(d[H]); return h } }, { key: "_initSerieVariables", value: function (t, e, i) { var a = this.w, s = new m(this.ctx); this.xDivision = a.globals.gridWidth / (a.globals.dataPoints - ("on" === a.config.xaxis.tickPlacement ? 1 : 0)), this.strokeWidth = Array.isArray(a.config.stroke.width) ? a.config.stroke.width[i] : a.config.stroke.width; var r = 0; this.yRatio.length > 1 && (this.yaxisIndex = a.globals.seriesYAxisReverseMap[i], r = i), this.isReversed = a.config.yaxis[this.yaxisIndex] && a.config.yaxis[this.yaxisIndex].reversed, this.zeroY = a.globals.gridHeight - this.baseLineY[r] - (this.isReversed ? a.globals.gridHeight : 0) + (this.isReversed ? 2 * this.baseLineY[r] : 0), this.areaBottomY = this.zeroY, (this.zeroY > a.globals.gridHeight || "end" === a.config.plotOptions.area.fillTo) && (this.areaBottomY = a.globals.gridHeight), this.categoryAxisCorrection = this.xDivision / 2, this.elSeries = s.group({ class: "apexcharts-series", zIndex: void 0 !== a.config.series[i].zIndex ? a.config.series[i].zIndex : i, seriesName: x.escapeString(a.globals.seriesNames[i]) }), this.elPointsMain = s.group({ class: "apexcharts-series-markers-wrap", "data:realIndex": i }), this.elDataLabelsWrap = s.group({ class: "apexcharts-datalabels", "data:realIndex": i }); var o = t[e].length === a.globals.dataPoints; this.elSeries.attr({ "data:longestSeries": o, rel: e + 1, "data:realIndex": i }), this.appendPathFrom = !0 } }, { key: "_calculatePathsFrom", value: function (t) { var e, i, a, s, r = t.type, o = t.series, n = t.i, l = t.realIndex, h = t.translationsIndex, c = t.prevX, d = t.prevY, g = t.prevY2, u = this.w, p = new m(this.ctx); if (null === o[n][0]) { for (var f = 0; f < o[n].length; f++)if (null !== o[n][f]) { c = this.xDivision * f, d = this.zeroY - o[n][f] / this.yRatio[h], e = p.move(c, d), i = p.move(c, this.areaBottomY); break } } else e = p.move(c, d), "rangeArea" === r && (e = p.move(c, g) + p.line(c, d)), i = p.move(c, this.areaBottomY) + p.line(c, d); if (a = p.move(0, this.zeroY) + p.line(0, this.zeroY), s = p.move(0, this.zeroY) + p.line(0, this.zeroY), u.globals.previousPaths.length > 0) { var x = this.lineHelpers.checkPreviousPaths({ pathFromLine: a, pathFromArea: s, realIndex: l }); a = x.pathFromLine, s = x.pathFromArea } return { prevX: c, prevY: d, linePath: e, areaPath: i, pathFromLine: a, pathFromArea: s } } }, { key: "_handlePaths", value: function (t) { var i = t.type, a = t.realIndex, s = t.i, r = t.paths, o = this.w, n = new m(this.ctx), l = new H(this.ctx); this.prevSeriesY.push(r.yArrj), o.globals.seriesXvalues[a] = r.xArrj, o.globals.seriesYvalues[a] = r.yArrj; var h = o.config.forecastDataPoints; if (h.count > 0 && "rangeArea" !== i) { var c = o.globals.seriesXvalues[a][o.globals.seriesXvalues[a].length - h.count - 1], d = n.drawRect(c, 0, o.globals.gridWidth, o.globals.gridHeight, 0); o.globals.dom.elForecastMask.appendChild(d.node); var g = n.drawRect(0, 0, c, o.globals.gridHeight, 0); o.globals.dom.elNonForecastMask.appendChild(g.node) } this.pointsChart || o.globals.delayedElements.push({ el: this.elPointsMain.node, index: a }); var u = { i: s, realIndex: a, animationDelay: s, initialSpeed: o.config.chart.animations.speed, dataChangeSpeed: o.config.chart.animations.dynamicAnimation.speed, className: "apexcharts-".concat(i) }; if ("area" === i) for (var p = l.fillPath({ seriesNumber: a }), f = 0; f < r.areaPaths.length; f++) { var x = n.renderPaths(e(e({}, u), {}, { pathFrom: r.pathFromArea, pathTo: r.areaPaths[f], stroke: "none", strokeWidth: 0, strokeLineCap: null, fill: p })); this.elSeries.add(x) } if (o.config.stroke.show && !this.pointsChart) { var b = null; if ("line" === i) b = l.fillPath({ seriesNumber: a, i: s }); else if ("solid" === o.config.stroke.fill.type) b = o.globals.stroke.colors[a]; else { var v = o.config.fill; o.config.fill = o.config.stroke.fill, b = l.fillPath({ seriesNumber: a, i: s }), o.config.fill = v } for (var y = 0; y < r.linePaths.length; y++) { var w = b; "rangeArea" === i && (w = l.fillPath({ seriesNumber: a })); var k = e(e({}, u), {}, { pathFrom: r.pathFromLine, pathTo: r.linePaths[y], stroke: b, strokeWidth: this.strokeWidth, strokeLineCap: o.config.stroke.lineCap, fill: "rangeArea" === i ? w : "none" }), A = n.renderPaths(k); if (this.elSeries.add(A), A.attr("fill-rule", "evenodd"), h.count > 0 && "rangeArea" !== i) { var S = n.renderPaths(k); S.node.setAttribute("stroke-dasharray", h.dashArray), h.strokeWidth && S.node.setAttribute("stroke-width", h.strokeWidth), this.elSeries.add(S), S.attr("clip-path", "url(#forecastMask".concat(o.globals.cuid, ")")), A.attr("clip-path", "url(#nonForecastMask".concat(o.globals.cuid, ")")) } } } } }, { key: "_iterateOverDataPoints", value: function (t) { var e, i, a = this, s = t.type, r = t.series, o = t.iterations, n = t.realIndex, l = t.translationsIndex, h = t.i, c = t.x, d = t.y, g = t.pX, u = t.pY, p = t.pathsFrom, f = t.linePaths, b = t.areaPaths, v = t.seriesIndex, y = t.lineYPosition, w = t.xArrj, k = t.yArrj, A = t.y2Arrj, S = t.isRangeStart, C = t.seriesRangeEnd, L = this.w, P = new m(this.ctx), M = this.yRatio, I = p.prevY, T = p.linePath, z = p.areaPath, X = p.pathFromLine, E = p.pathFromArea, Y = x.isNumber(L.globals.minYArr[n]) ? L.globals.minYArr[n] : L.globals.minY; o || (o = L.globals.dataPoints > 1 ? L.globals.dataPoints - 1 : L.globals.dataPoints); var F = function (t, e) { return e - t / M[l] + 2 * (a.isReversed ? t / M[l] : 0) }, R = d, H = L.config.chart.stacked && !L.globals.comboCharts || L.config.chart.stacked && L.globals.comboCharts && (!this.w.config.chart.stackOnlyBar || "bar" === (null === (e = this.w.config.series[n]) || void 0 === e ? void 0 : e.type) || "column" === (null === (i = this.w.config.series[n]) || void 0 === i ? void 0 : i.type)), D = L.config.stroke.curve; Array.isArray(D) && (D = Array.isArray(v) ? D[v[h]] : D[h]); for (var O, N = 0, W = 0; W < o; W++) { var B = void 0 === r[h][W + 1] || null === r[h][W + 1]; if (L.globals.isXNumeric) { var G = L.globals.seriesX[n][W + 1]; void 0 === L.globals.seriesX[n][W + 1] && (G = L.globals.seriesX[n][o - 1]), c = (G - L.globals.minX) / this.xRatio } else c += this.xDivision; if (H) if (h > 0 && L.globals.collapsedSeries.length < L.config.series.length - 1) { y = this.prevSeriesY[function (t) { for (var e = t; e > 0; e--) { if (!(L.globals.collapsedSeriesIndices.indexOf((null == v ? void 0 : v[e]) || e) > -1)) return e; e-- } return 0 }(h - 1)][W + 1] } else y = this.zeroY; else y = this.zeroY; B ? d = F(Y, y) : (d = F(r[h][W + 1], y), "rangeArea" === s && (R = F(C[h][W + 1], y))), w.push(c), !B || "smooth" !== L.config.stroke.curve && "monotoneCubic" !== L.config.stroke.curve ? (k.push(d), A.push(R)) : (k.push(null), A.push(null)); var V = this.lineHelpers.calculatePoints({ series: r, x: c, y: d, realIndex: n, i: h, j: W, prevY: I }), j = this._createPaths({ type: s, series: r, i: h, realIndex: n, j: W, x: c, y: d, y2: R, xArrj: w, yArrj: k, y2Arrj: A, pX: g, pY: u, pathState: N, segmentStartX: O, linePath: T, areaPath: z, linePaths: f, areaPaths: b, curve: D, isRangeStart: S }); b = j.areaPaths, f = j.linePaths, g = j.pX, u = j.pY, N = j.pathState, O = j.segmentStartX, z = j.areaPath, T = j.linePath, !this.appendPathFrom || "monotoneCubic" === D && "rangeArea" === s || (X += P.line(c, this.zeroY), E += P.line(c, this.zeroY)), this.handleNullDataPoints(r, V, h, W, n), this._handleMarkersAndLabels({ type: s, pointsPos: V, i: h, j: W, realIndex: n, isRangeStart: S }) } return { yArrj: k, xArrj: w, pathFromArea: E, areaPaths: b, pathFromLine: X, linePaths: f, linePath: T, areaPath: z } } }, { key: "_handleMarkersAndLabels", value: function (t) { var e = t.type, i = t.pointsPos, a = t.isRangeStart, s = t.i, r = t.j, o = t.realIndex, n = this.w, l = new N(this.ctx); if (this.pointsChart) this.scatter.draw(this.elSeries, r, { realIndex: o, pointsPos: i, zRatio: this.zRatio, elParent: this.elPointsMain }); else { n.globals.series[s].length > 1 && this.elPointsMain.node.classList.add("apexcharts-element-hidden"); var h = this.markers.plotChartMarkers(i, o, r + 1); null !== h && this.elPointsMain.add(h) } var c = l.drawDataLabel({ type: e, isRangeStart: a, pos: i, i: o, j: r + 1 }); null !== c && this.elDataLabelsWrap.add(c) } }, { key: "_createPaths", value: function (t) { var e = t.type, i = t.series, a = t.i; t.realIndex; var s = t.j, r = t.x, o = t.y, n = t.xArrj, l = t.yArrj, h = t.y2, c = t.y2Arrj, d = t.pX, g = t.pY, u = t.pathState, p = t.segmentStartX, f = t.linePath, x = t.areaPath, b = t.linePaths, v = t.areaPaths, y = t.curve, w = t.isRangeStart; this.w; var k, A = new m(this.ctx), S = this.areaBottomY, C = "rangeArea" === e, L = "rangeArea" === e && w; switch (y) { case "monotoneCubic": var P = w ? l : c; switch (u) { case 0: if (null === P[s + 1]) break; u = 1; case 1: if (!(C ? n.length === i[a].length : s === i[a].length - 2)) break; case 2: var M = w ? n : n.slice().reverse(), I = w ? P : P.slice().reverse(), T = (k = I, M.map((function (t, e) { return [t, k[e]] })).filter((function (t) { return null !== t[1] }))), z = T.length > 1 ? Xt(T) : T, X = []; C && (L ? v = T : X = v.reverse()); var E = 0, Y = 0; if (function (t, e) { for (var i = function (t) { var e = [], i = 0; return t.forEach((function (t) { null !== t ? i++ : i > 0 && (e.push(i), i = 0) })), i > 0 && e.push(i), e }(t), a = [], s = 0, r = 0; s < i.length; r += i[s++])a[s] = Et(e, r, r + i[s]); return a }(I, z).forEach((function (t) { E++; var e = function (t) { for (var e = "", i = 0; i < t.length; i++) { var a = t[i], s = a.length; s > 4 ? (e += "C".concat(a[0], ", ").concat(a[1]), e += ", ".concat(a[2], ", ").concat(a[3]), e += ", ".concat(a[4], ", ").concat(a[5])) : s > 2 && (e += "S".concat(a[0], ", ").concat(a[1]), e += ", ".concat(a[2], ", ").concat(a[3])) } return e }(t), i = Y, a = (Y += t.length) - 1; L ? f = A.move(T[i][0], T[i][1]) + e : C ? f = A.move(X[i][0], X[i][1]) + A.line(T[i][0], T[i][1]) + e + A.line(X[a][0], X[a][1]) : (f = A.move(T[i][0], T[i][1]) + e, x = f + A.line(T[a][0], S) + A.line(T[i][0], S) + "z", v.push(x)), b.push(f) })), C && E > 1 && !L) { var F = b.slice(E).reverse(); b.splice(E), F.forEach((function (t) { return b.push(t) })) } u = 0 }break; case "smooth": var R = .35 * (r - d); if (null === i[a][s]) u = 0; else switch (u) { case 0: if (p = d, f = L ? A.move(d, c[s]) + A.line(d, g) : A.move(d, g), x = A.move(d, g), u = 1, s < i[a].length - 2) { var H = A.curve(d + R, g, r - R, o, r, o); f += H, x += H; break } case 1: if (null === i[a][s + 1]) f += L ? A.line(d, h) : A.move(d, g), x += A.line(d, S) + A.line(p, S) + "z", b.push(f), v.push(x); else { var D = A.curve(d + R, g, r - R, o, r, o); f += D, x += D, s >= i[a].length - 2 && (L && (f += A.curve(r, o, r, o, r, h) + A.move(r, h)), x += A.curve(r, o, r, o, r, S) + A.line(p, S) + "z", b.push(f), v.push(x)) } }d = r, g = o; break; default: var O = function (t, e, i) { var a = []; switch (t) { case "stepline": a = A.line(e, null, "H") + A.line(null, i, "V"); break; case "linestep": a = A.line(null, i, "V") + A.line(e, null, "H"); break; case "straight": a = A.line(e, i) }return a }; if (null === i[a][s]) u = 0; else switch (u) { case 0: if (p = d, f = L ? A.move(d, c[s]) + A.line(d, g) : A.move(d, g), x = A.move(d, g), u = 1, s < i[a].length - 2) { var N = O(y, r, o); f += N, x += N; break } case 1: if (null === i[a][s + 1]) f += L ? A.line(d, h) : A.move(d, g), x += A.line(d, S) + A.line(p, S) + "z", b.push(f), v.push(x); else { var W = O(y, r, o); f += W, x += W, s >= i[a].length - 2 && (L && (f += A.line(r, h)), x += A.line(r, S) + A.line(p, S) + "z", b.push(f), v.push(x)) } }d = r, g = o }return { linePaths: b, areaPaths: v, pX: d, pY: g, pathState: u, segmentStartX: p, linePath: f, areaPath: x } } }, { key: "handleNullDataPoints", value: function (t, e, i, a, s) { var r = this.w; if (null === t[i][a] && r.config.markers.showNullDataPoints || 1 === t[i].length) { var o = this.strokeWidth - r.config.markers.strokeWidth / 2; o > 0 || (o = 0); var n = this.markers.plotChartMarkers(e, s, a + 1, o, !0); null !== n && this.elPointsMain.add(n) } } }]), t }(); window.TreemapSquared = {}, window.TreemapSquared.generate = function () { function t(e, i, a, s) { this.xoffset = e, this.yoffset = i, this.height = s, this.width = a, this.shortestEdge = function () { return Math.min(this.height, this.width) }, this.getCoordinates = function (t) { var e, i = [], a = this.xoffset, s = this.yoffset, o = r(t) / this.height, n = r(t) / this.width; if (this.width >= this.height) for (e = 0; e < t.length; e++)i.push([a, s, a + o, s + t[e] / o]), s += t[e] / o; else for (e = 0; e < t.length; e++)i.push([a, s, a + t[e] / n, s + n]), a += t[e] / n; return i }, this.cutArea = function (e) { var i; if (this.width >= this.height) { var a = e / this.height, s = this.width - a; i = new t(this.xoffset + a, this.yoffset, s, this.height) } else { var r = e / this.width, o = this.height - r; i = new t(this.xoffset, this.yoffset + r, this.width, o) } return i } } function e(e, a, s, o, n) { o = void 0 === o ? 0 : o, n = void 0 === n ? 0 : n; var l = i(function (t, e) { var i, a = [], s = e / r(t); for (i = 0; i < t.length; i++)a[i] = t[i] * s; return a }(e, a * s), [], new t(o, n, a, s), []); return function (t) { var e, i, a = []; for (e = 0; e < t.length; e++)for (i = 0; i < t[e].length; i++)a.push(t[e][i]); return a }(l) } function i(t, e, s, o) { var n, l, h; if (0 !== t.length) return n = s.shortestEdge(), function (t, e, i) { var s; if (0 === t.length) return !0; (s = t.slice()).push(e); var r = a(t, i), o = a(s, i); return r >= o }(e, l = t[0], n) ? (e.push(l), i(t.slice(1), e, s, o)) : (h = s.cutArea(r(e), o), o.push(s.getCoordinates(e)), i(t, [], h, o)), o; o.push(s.getCoordinates(e)) } function a(t, e) { var i = Math.min.apply(Math, t), a = Math.max.apply(Math, t), s = r(t); return Math.max(Math.pow(e, 2) * a / Math.pow(s, 2), Math.pow(s, 2) / (Math.pow(e, 2) * i)) } function s(t) { return t && t.constructor === Array } function r(t) { var e, i = 0; for (e = 0; e < t.length; e++)i += t[e]; return i } function o(t) { var e, i = 0; if (s(t[0])) for (e = 0; e < t.length; e++)i += o(t[e]); else i = r(t); return i } return function t(i, a, r, n, l) { n = void 0 === n ? 0 : n, l = void 0 === l ? 0 : l; var h, c, d = [], g = []; if (s(i[0])) { for (c = 0; c < i.length; c++)d[c] = o(i[c]); for (h = e(d, a, r, n, l), c = 0; c < i.length; c++)g.push(t(i[c], h[c][2] - h[c][0], h[c][3] - h[c][1], h[c][0], h[c][1])) } else g = e(i, a, r, n, l); return g } }(); var Rt, Ht, Dt = function () { function t(e, i) { a(this, t), this.ctx = e, this.w = e.w, this.strokeWidth = this.w.config.stroke.width, this.helpers = new At(e), this.dynamicAnim = this.w.config.chart.animations.dynamicAnimation, this.labels = [] } return r(t, [{ key: "draw", value: function (t) { var e = this, i = this.w, a = new m(this.ctx), s = new H(this.ctx), r = a.group({ class: "apexcharts-treemap" }); if (i.globals.noData) return r; var o = []; return t.forEach((function (t) { var e = t.map((function (t) { return Math.abs(t) })); o.push(e) })), this.negRange = this.helpers.checkColorRange(), i.config.series.forEach((function (t, i) { t.data.forEach((function (t) { Array.isArray(e.labels[i]) || (e.labels[i] = []), e.labels[i].push(t.x) })) })), window.TreemapSquared.generate(o, i.globals.gridWidth, i.globals.gridHeight).forEach((function (o, n) { var l = a.group({ class: "apexcharts-series apexcharts-treemap-series", seriesName: x.escapeString(i.globals.seriesNames[n]), rel: n + 1, "data:realIndex": n }); if (i.config.chart.dropShadow.enabled) { var h = i.config.chart.dropShadow; new v(e.ctx).dropShadow(r, h, n) } var c = a.group({ class: "apexcharts-data-labels" }); o.forEach((function (r, o) { var h = r[0], c = r[1], d = r[2], g = r[3], u = a.drawRect(h, c, d - h, g - c, i.config.plotOptions.treemap.borderRadius, "#fff", 1, e.strokeWidth, i.config.plotOptions.treemap.useFillColorAsStroke ? f : i.globals.stroke.colors[n]); u.attr({ cx: h, cy: c, index: n, i: n, j: o, width: d - h, height: g - c }); var p = e.helpers.getShadeColor(i.config.chart.type, n, o, e.negRange), f = p.color; void 0 !== i.config.series[n].data[o] && i.config.series[n].data[o].fillColor && (f = i.config.series[n].data[o].fillColor); var x = s.fillPath({ color: f, seriesNumber: n, dataPointIndex: o }); u.node.classList.add("apexcharts-treemap-rect"), u.attr({ fill: x }), e.helpers.addListeners(u); var b = { x: h + (d - h) / 2, y: c + (g - c) / 2, width: 0, height: 0 }, v = { x: h, y: c, width: d - h, height: g - c }; if (i.config.chart.animations.enabled && !i.globals.dataChanged) { var m = 1; i.globals.resized || (m = i.config.chart.animations.speed), e.animateTreemap(u, b, v, m) } if (i.globals.dataChanged) { var y = 1; e.dynamicAnim.enabled && i.globals.shouldAnimate && (y = e.dynamicAnim.speed, i.globals.previousPaths[n] && i.globals.previousPaths[n][o] && i.globals.previousPaths[n][o].rect && (b = i.globals.previousPaths[n][o].rect), e.animateTreemap(u, b, v, y)) } var w = e.getFontSize(r), k = i.config.dataLabels.formatter(e.labels[n][o], { value: i.globals.series[n][o], seriesIndex: n, dataPointIndex: o, w: i }); "truncate" === i.config.plotOptions.treemap.dataLabels.format && (w = parseInt(i.config.dataLabels.style.fontSize, 10), k = e.truncateLabels(k, w, h, c, d, g)); var A = e.helpers.calculateDataLabels({ text: k, x: (h + d) / 2, y: (c + g) / 2 + e.strokeWidth / 2 + w / 3, i: n, j: o, colorProps: p, fontSize: w, series: t }); i.config.dataLabels.enabled && A && e.rotateToFitLabel(A, w, k, h, c, d, g), l.add(u), null !== A && l.add(A) })), l.add(c), r.add(l) })), r } }, { key: "getFontSize", value: function (t) { var e = this.w; var i, a, s, r, o = function t(e) { var i, a = 0; if (Array.isArray(e[0])) for (i = 0; i < e.length; i++)a += t(e[i]); else for (i = 0; i < e.length; i++)a += e[i].length; return a }(this.labels) / function t(e) { var i, a = 0; if (Array.isArray(e[0])) for (i = 0; i < e.length; i++)a += t(e[i]); else for (i = 0; i < e.length; i++)a += 1; return a }(this.labels); return i = t[2] - t[0], a = t[3] - t[1], s = i * a, r = Math.pow(s, .5), Math.min(r / o, parseInt(e.config.dataLabels.style.fontSize, 10)) } }, { key: "rotateToFitLabel", value: function (t, e, i, a, s, r, o) { var n = new m(this.ctx), l = n.getTextRects(i, e); if (l.width + this.w.config.stroke.width + 5 > r - a && l.width <= o - s) { var h = n.rotateAroundCenter(t.node); t.node.setAttribute("transform", "rotate(-90 ".concat(h.x, " ").concat(h.y, ") translate(").concat(l.height / 3, ")")) } } }, { key: "truncateLabels", value: function (t, e, i, a, s, r) { var o = new m(this.ctx), n = o.getTextRects(t, e).width + this.w.config.stroke.width + 5 > s - i && r - a > s - i ? r - a : s - i, l = o.getTextBasedOnMaxWidth({ text: t, maxWidth: n, fontSize: e }); return t.length !== l.length && n / e < 5 ? "" : l } }, { key: "animateTreemap", value: function (t, e, i, a) { var s = new b(this.ctx); s.animateRect(t, { x: e.x, y: e.y, width: e.width, height: e.height }, { x: i.x, y: i.y, width: i.width, height: i.height }, a, (function () { s.animationCompleted(t) })) } }]), t }(), Ot = 86400, Nt = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w, this.timeScaleArray = [], this.utc = this.w.config.xaxis.labels.datetimeUTC } return r(t, [{ key: "calculateTimeScaleTicks", value: function (t, i) { var a = this, s = this.w; if (s.globals.allSeriesCollapsed) return s.globals.labels = [], s.globals.timescaleLabels = [], []; var r = new A(this.ctx), o = (i - t) / 864e5; this.determineInterval(o), s.globals.disableZoomIn = !1, s.globals.disableZoomOut = !1, o < .00011574074074074075 ? s.globals.disableZoomIn = !0 : o > 5e4 && (s.globals.disableZoomOut = !0); var n = r.getTimeUnitsfromTimestamp(t, i, this.utc), l = s.globals.gridWidth / o, h = l / 24, c = h / 60, d = c / 60, g = Math.floor(24 * o), u = Math.floor(1440 * o), p = Math.floor(o * Ot), f = Math.floor(o), x = Math.floor(o / 30), b = Math.floor(o / 365), v = { minMillisecond: n.minMillisecond, minSecond: n.minSecond, minMinute: n.minMinute, minHour: n.minHour, minDate: n.minDate, minMonth: n.minMonth, minYear: n.minYear }, m = { firstVal: v, currentMillisecond: v.minMillisecond, currentSecond: v.minSecond, currentMinute: v.minMinute, currentHour: v.minHour, currentMonthDate: v.minDate, currentDate: v.minDate, currentMonth: v.minMonth, currentYear: v.minYear, daysWidthOnXAxis: l, hoursWidthOnXAxis: h, minutesWidthOnXAxis: c, secondsWidthOnXAxis: d, numberOfSeconds: p, numberOfMinutes: u, numberOfHours: g, numberOfDays: f, numberOfMonths: x, numberOfYears: b }; switch (this.tickInterval) { case "years": this.generateYearScale(m); break; case "months": case "half_year": this.generateMonthScale(m); break; case "months_days": case "months_fortnight": case "days": case "week_days": this.generateDayScale(m); break; case "hours": this.generateHourScale(m); break; case "minutes_fives": case "minutes": this.generateMinuteScale(m); break; case "seconds_tens": case "seconds_fives": case "seconds": this.generateSecondScale(m) }var y = this.timeScaleArray.map((function (t) { var i = { position: t.position, unit: t.unit, year: t.year, day: t.day ? t.day : 1, hour: t.hour ? t.hour : 0, month: t.month + 1 }; return "month" === t.unit ? e(e({}, i), {}, { day: 1, value: t.value + 1 }) : "day" === t.unit || "hour" === t.unit ? e(e({}, i), {}, { value: t.value }) : "minute" === t.unit ? e(e({}, i), {}, { value: t.value, minute: t.value }) : "second" === t.unit ? e(e({}, i), {}, { value: t.value, minute: t.minute, second: t.second }) : t })); return y.filter((function (t) { var e = 1, i = Math.ceil(s.globals.gridWidth / 120), r = t.value; void 0 !== s.config.xaxis.tickAmount && (i = s.config.xaxis.tickAmount), y.length > i && (e = Math.floor(y.length / i)); var o = !1, n = !1; switch (a.tickInterval) { case "years": "year" === t.unit && (o = !0); break; case "half_year": e = 7, "year" === t.unit && (o = !0); break; case "months": e = 1, "year" === t.unit && (o = !0); break; case "months_fortnight": e = 15, "year" !== t.unit && "month" !== t.unit || (o = !0), 30 === r && (n = !0); break; case "months_days": e = 10, "month" === t.unit && (o = !0), 30 === r && (n = !0); break; case "week_days": e = 8, "month" === t.unit && (o = !0); break; case "days": e = 1, "month" === t.unit && (o = !0); break; case "hours": "day" === t.unit && (o = !0); break; case "minutes_fives": case "seconds_fives": r % 5 != 0 && (n = !0); break; case "seconds_tens": r % 10 != 0 && (n = !0) }if ("hours" === a.tickInterval || "minutes_fives" === a.tickInterval || "seconds_tens" === a.tickInterval || "seconds_fives" === a.tickInterval) { if (!n) return !0 } else if ((r % e == 0 || o) && !n) return !0 })) } }, { key: "recalcDimensionsBasedOnFormat", value: function (t, e) { var i = this.w, a = this.formatDates(t), s = this.removeOverlappingTS(a); i.globals.timescaleLabels = s.slice(), new ot(this.ctx).plotCoords() } }, { key: "determineInterval", value: function (t) { var e = 24 * t, i = 60 * e; switch (!0) { case t / 365 > 5: this.tickInterval = "years"; break; case t > 800: this.tickInterval = "half_year"; break; case t > 180: this.tickInterval = "months"; break; case t > 90: this.tickInterval = "months_fortnight"; break; case t > 60: this.tickInterval = "months_days"; break; case t > 30: this.tickInterval = "week_days"; break; case t > 2: this.tickInterval = "days"; break; case e > 2.4: this.tickInterval = "hours"; break; case i > 15: this.tickInterval = "minutes_fives"; break; case i > 5: this.tickInterval = "minutes"; break; case i > 1: this.tickInterval = "seconds_tens"; break; case 60 * i > 20: this.tickInterval = "seconds_fives"; break; default: this.tickInterval = "seconds" } } }, { key: "generateYearScale", value: function (t) { var e = t.firstVal, i = t.currentMonth, a = t.currentYear, s = t.daysWidthOnXAxis, r = t.numberOfYears, o = e.minYear, n = 0, l = new A(this.ctx), h = "year"; if (e.minDate > 1 || e.minMonth > 0) { var c = l.determineRemainingDaysOfYear(e.minYear, e.minMonth, e.minDate); n = (l.determineDaysOfYear(e.minYear) - c + 1) * s, o = e.minYear + 1, this.timeScaleArray.push({ position: n, value: o, unit: h, year: o, month: x.monthMod(i + 1) }) } else 1 === e.minDate && 0 === e.minMonth && this.timeScaleArray.push({ position: n, value: o, unit: h, year: a, month: x.monthMod(i + 1) }); for (var d = o, g = n, u = 0; u < r; u++)d++, g = l.determineDaysOfYear(d - 1) * s + g, this.timeScaleArray.push({ position: g, value: d, unit: h, year: d, month: 1 }) } }, { key: "generateMonthScale", value: function (t) { var e = t.firstVal, i = t.currentMonthDate, a = t.currentMonth, s = t.currentYear, r = t.daysWidthOnXAxis, o = t.numberOfMonths, n = a, l = 0, h = new A(this.ctx), c = "month", d = 0; if (e.minDate > 1) { l = (h.determineDaysOfMonths(a + 1, e.minYear) - i + 1) * r, n = x.monthMod(a + 1); var g = s + d, u = x.monthMod(n), p = n; 0 === n && (c = "year", p = g, u = 1, g += d += 1), this.timeScaleArray.push({ position: l, value: p, unit: c, year: g, month: u }) } else this.timeScaleArray.push({ position: l, value: n, unit: c, year: s, month: x.monthMod(a) }); for (var f = n + 1, b = l, v = 0, m = 1; v < o; v++, m++) { 0 === (f = x.monthMod(f)) ? (c = "year", d += 1) : c = "month"; var y = this._getYear(s, f, d); b = h.determineDaysOfMonths(f, y) * r + b; var w = 0 === f ? y : f; this.timeScaleArray.push({ position: b, value: w, unit: c, year: y, month: 0 === f ? 1 : f }), f++ } } }, { key: "generateDayScale", value: function (t) { var e = t.firstVal, i = t.currentMonth, a = t.currentYear, s = t.hoursWidthOnXAxis, r = t.numberOfDays, o = new A(this.ctx), n = "day", l = e.minDate + 1, h = l, c = function (t, e, i) { return t > o.determineDaysOfMonths(e + 1, i) ? (h = 1, n = "month", g = e += 1, e) : e }, d = (24 - e.minHour) * s, g = l, u = c(h, i, a); 0 === e.minHour && 1 === e.minDate ? (d = 0, g = x.monthMod(e.minMonth), n = "month", h = e.minDate) : 1 !== e.minDate && 0 === e.minHour && 0 === e.minMinute && (d = 0, l = e.minDate, g = l, u = c(h = l, i, a)), this.timeScaleArray.push({ position: d, value: g, unit: n, year: this._getYear(a, u, 0), month: x.monthMod(u), day: h }); for (var p = d, f = 0; f < r; f++) { n = "day", u = c(h += 1, u, this._getYear(a, u, 0)); var b = this._getYear(a, u, 0); p = 24 * s + p; var v = 1 === h ? x.monthMod(u) : h; this.timeScaleArray.push({ position: p, value: v, unit: n, year: b, month: x.monthMod(u), day: v }) } } }, { key: "generateHourScale", value: function (t) { var e = t.firstVal, i = t.currentDate, a = t.currentMonth, s = t.currentYear, r = t.minutesWidthOnXAxis, o = t.numberOfHours, n = new A(this.ctx), l = "hour", h = function (t, e) { return t > n.determineDaysOfMonths(e + 1, s) && (f = 1, e += 1), { month: e, date: f } }, c = function (t, e) { return t > n.determineDaysOfMonths(e + 1, s) ? e += 1 : e }, d = 60 - (e.minMinute + e.minSecond / 60), g = d * r, u = e.minHour + 1, p = u; 60 === d && (g = 0, p = u = e.minHour); var f = i; p >= 24 && (p = 0, f += 1, l = "day"); var b = h(f, a).month; b = c(f, b), this.timeScaleArray.push({ position: g, value: u, unit: l, day: f, hour: p, year: s, month: x.monthMod(b) }), p++; for (var v = g, m = 0; m < o; m++) { if (l = "hour", p >= 24) p = 0, l = "day", b = h(f += 1, b).month, b = c(f, b); var y = this._getYear(s, b, 0); v = 60 * r + v; var w = 0 === p ? f : p; this.timeScaleArray.push({ position: v, value: w, unit: l, hour: p, day: f, year: y, month: x.monthMod(b) }), p++ } } }, { key: "generateMinuteScale", value: function (t) { for (var e = t.currentMillisecond, i = t.currentSecond, a = t.currentMinute, s = t.currentHour, r = t.currentDate, o = t.currentMonth, n = t.currentYear, l = t.minutesWidthOnXAxis, h = t.secondsWidthOnXAxis, c = t.numberOfMinutes, d = a + 1, g = r, u = o, p = n, f = s, b = (60 - i - e / 1e3) * h, v = 0; v < c; v++)d >= 60 && (d = 0, 24 === (f += 1) && (f = 0)), this.timeScaleArray.push({ position: b, value: d, unit: "minute", hour: f, minute: d, day: g, year: this._getYear(p, u, 0), month: x.monthMod(u) }), b += l, d++ } }, { key: "generateSecondScale", value: function (t) { for (var e = t.currentMillisecond, i = t.currentSecond, a = t.currentMinute, s = t.currentHour, r = t.currentDate, o = t.currentMonth, n = t.currentYear, l = t.secondsWidthOnXAxis, h = t.numberOfSeconds, c = i + 1, d = a, g = r, u = o, p = n, f = s, b = (1e3 - e) / 1e3 * l, v = 0; v < h; v++)c >= 60 && (c = 0, ++d >= 60 && (d = 0, 24 === ++f && (f = 0))), this.timeScaleArray.push({ position: b, value: c, unit: "second", hour: f, minute: d, second: c, day: g, year: this._getYear(p, u, 0), month: x.monthMod(u) }), b += l, c++ } }, { key: "createRawDateString", value: function (t, e) { var i = t.year; return 0 === t.month && (t.month = 1), i += "-" + ("0" + t.month.toString()).slice(-2), "day" === t.unit ? i += "day" === t.unit ? "-" + ("0" + e).slice(-2) : "-01" : i += "-" + ("0" + (t.day ? t.day : "1")).slice(-2), "hour" === t.unit ? i += "hour" === t.unit ? "T" + ("0" + e).slice(-2) : "T00" : i += "T" + ("0" + (t.hour ? t.hour : "0")).slice(-2), "minute" === t.unit ? i += ":" + ("0" + e).slice(-2) : i += ":" + (t.minute ? ("0" + t.minute).slice(-2) : "00"), "second" === t.unit ? i += ":" + ("0" + e).slice(-2) : i += ":00", this.utc && (i += ".000Z"), i } }, { key: "formatDates", value: function (t) { var e = this, i = this.w; return t.map((function (t) { var a = t.value.toString(), s = new A(e.ctx), r = e.createRawDateString(t, a), o = s.getDate(s.parseDate(r)); if (e.utc || (o = s.getDate(s.parseDateWithTimezone(r))), void 0 === i.config.xaxis.labels.format) { var n = "dd MMM", l = i.config.xaxis.labels.datetimeFormatter; "year" === t.unit && (n = l.year), "month" === t.unit && (n = l.month), "day" === t.unit && (n = l.day), "hour" === t.unit && (n = l.hour), "minute" === t.unit && (n = l.minute), "second" === t.unit && (n = l.second), a = s.formatDate(o, n) } else a = s.formatDate(o, i.config.xaxis.labels.format); return { dateString: r, position: t.position, value: a, unit: t.unit, year: t.year, month: t.month } })) } }, { key: "removeOverlappingTS", value: function (t) { var e, i = this, a = new m(this.ctx), s = !1; t.length > 0 && t[0].value && t.every((function (e) { return e.value.length === t[0].value.length })) && (s = !0, e = a.getTextRects(t[0].value).width); var r = 0, o = t.map((function (o, n) { if (n > 0 && i.w.config.xaxis.labels.hideOverlappingLabels) { var l = s ? e : a.getTextRects(t[r].value).width, h = t[r].position; return o.position > h + l + 10 ? (r = n, o) : null } return o })); return o = o.filter((function (t) { return null !== t })) } }, { key: "_getYear", value: function (t, e, i) { return t + Math.floor(e / 12) + i } }]), t }(), Wt = function () { function t(e, i) { a(this, t), this.ctx = i, this.w = i.w, this.el = e } return r(t, [{ key: "setupElements", value: function () { var t = this.w.globals, e = this.w.config, i = e.chart.type; t.axisCharts = ["line", "area", "bar", "rangeBar", "rangeArea", "candlestick", "boxPlot", "scatter", "bubble", "radar", "heatmap", "treemap"].indexOf(i) > -1, t.xyCharts = ["line", "area", "bar", "rangeBar", "rangeArea", "candlestick", "boxPlot", "scatter", "bubble"].indexOf(i) > -1, t.isBarHorizontal = ("bar" === e.chart.type || "rangeBar" === e.chart.type || "boxPlot" === e.chart.type) && e.plotOptions.bar.horizontal, t.chartClass = ".apexcharts" + t.chartID, t.dom.baseEl = this.el, t.dom.elWrap = document.createElement("div"), m.setAttrs(t.dom.elWrap, { id: t.chartClass.substring(1), class: "apexcharts-canvas " + t.chartClass.substring(1) }), this.el.appendChild(t.dom.elWrap), t.dom.Paper = new window.SVG.Doc(t.dom.elWrap), t.dom.Paper.attr({ class: "apexcharts-svg", "xmlns:data": "ApexChartsNS", transform: "translate(".concat(e.chart.offsetX, ", ").concat(e.chart.offsetY, ")") }), t.dom.Paper.node.style.background = "dark" !== e.theme.mode || e.chart.background ? e.chart.background : "rgba(0, 0, 0, 0.8)", this.setSVGDimensions(), t.dom.elLegendForeign = document.createElementNS(t.SVGNS, "foreignObject"), m.setAttrs(t.dom.elLegendForeign, { x: 0, y: 0, width: t.svgWidth, height: t.svgHeight }), t.dom.elLegendWrap = document.createElement("div"), t.dom.elLegendWrap.classList.add("apexcharts-legend"), t.dom.elLegendWrap.setAttribute("xmlns", "http://www.w3.org/1999/xhtml"), t.dom.elLegendForeign.appendChild(t.dom.elLegendWrap), t.dom.Paper.node.appendChild(t.dom.elLegendForeign), t.dom.elGraphical = t.dom.Paper.group().attr({ class: "apexcharts-inner apexcharts-graphical" }), t.dom.elDefs = t.dom.Paper.defs(), t.dom.Paper.add(t.dom.elGraphical), t.dom.elGraphical.add(t.dom.elDefs) } }, { key: "plotChartType", value: function (t, e) { var i = this.w, a = i.config, s = i.globals, r = { series: [], i: [] }, o = { series: [], i: [] }, n = { series: [], i: [] }, l = { series: [], i: [] }, h = { series: [], i: [] }, c = { series: [], i: [] }, d = { series: [], i: [] }, g = { series: [], i: [] }, p = { series: [], seriesRangeEnd: [], i: [] }, f = void 0 !== a.chart.type ? a.chart.type : "line", x = null, b = 0; s.series.forEach((function (e, a) { var u = t[a].type || f; switch (u) { case "column": case "bar": h.series.push(e), h.i.push(a), i.globals.columnSeries = h; break; case "area": o.series.push(e), o.i.push(a); break; case "line": r.series.push(e), r.i.push(a); break; case "scatter": n.series.push(e), n.i.push(a); break; case "bubble": l.series.push(e), l.i.push(a); break; case "candlestick": c.series.push(e), c.i.push(a); break; case "boxPlot": d.series.push(e), d.i.push(a); break; case "rangeBar": g.series.push(e), g.i.push(a); break; case "rangeArea": p.series.push(s.seriesRangeStart[a]), p.seriesRangeEnd.push(s.seriesRangeEnd[a]), p.i.push(a); break; case "heatmap": case "treemap": case "pie": case "donut": case "polarArea": case "radialBar": case "radar": x = u; break; default: console.warn("You have specified an unrecognized series type (", u, ").") }f !== u && "scatter" !== u && b++ })), b > 0 && (null !== x && console.warn("Chart or series type ", x, " can not appear with other chart or series types."), h.series.length > 0 && a.plotOptions.bar.horizontal && (b -= h.length, h = { series: [], i: [] }, i.globals.columnSeries = { series: [], i: [] }, console.warn("Horizontal bars are not supported in a mixed/combo chart. Please turn off `plotOptions.bar.horizontal`"))), s.comboCharts || (s.comboCharts = b > 0); var v = new Ft(this.ctx, e), m = new kt(this.ctx, e); this.ctx.pie = new Lt(this.ctx); var w = new Mt(this.ctx); this.ctx.rangeBar = new It(this.ctx, e); var k = new Pt(this.ctx), A = []; if (s.comboCharts) { var S, C, L = new y(this.ctx); if (o.series.length > 0) (S = A).push.apply(S, u(L.drawSeriesByGroup(o, s.areaGroups, "area", v))); if (h.series.length > 0) if (i.config.chart.stacked) { var P = new wt(this.ctx, e); A.push(P.draw(h.series, h.i)) } else this.ctx.bar = new yt(this.ctx, e), A.push(this.ctx.bar.draw(h.series, h.i)); if (p.series.length > 0 && A.push(v.draw(p.series, "rangeArea", p.i, p.seriesRangeEnd)), r.series.length > 0) (C = A).push.apply(C, u(L.drawSeriesByGroup(r, s.lineGroups, "line", v))); if (c.series.length > 0 && A.push(m.draw(c.series, "candlestick", c.i)), d.series.length > 0 && A.push(m.draw(d.series, "boxPlot", d.i)), g.series.length > 0 && A.push(this.ctx.rangeBar.draw(g.series, g.i)), n.series.length > 0) { var M = new Ft(this.ctx, e, !0); A.push(M.draw(n.series, "scatter", n.i)) } if (l.series.length > 0) { var I = new Ft(this.ctx, e, !0); A.push(I.draw(l.series, "bubble", l.i)) } } else switch (a.chart.type) { case "line": A = v.draw(s.series, "line"); break; case "area": A = v.draw(s.series, "area"); break; case "bar": if (a.chart.stacked) A = new wt(this.ctx, e).draw(s.series); else this.ctx.bar = new yt(this.ctx, e), A = this.ctx.bar.draw(s.series); break; case "candlestick": A = new kt(this.ctx, e).draw(s.series, "candlestick"); break; case "boxPlot": A = new kt(this.ctx, e).draw(s.series, a.chart.type); break; case "rangeBar": A = this.ctx.rangeBar.draw(s.series); break; case "rangeArea": A = v.draw(s.seriesRangeStart, "rangeArea", void 0, s.seriesRangeEnd); break; case "heatmap": A = new St(this.ctx, e).draw(s.series); break; case "treemap": A = new Dt(this.ctx, e).draw(s.series); break; case "pie": case "donut": case "polarArea": A = this.ctx.pie.draw(s.series); break; case "radialBar": A = w.draw(s.series); break; case "radar": A = k.draw(s.series); break; default: A = v.draw(s.series) }return A } }, { key: "setSVGDimensions", value: function () { var t = this.w.globals, e = this.w.config; t.svgWidth = e.chart.width, t.svgHeight = e.chart.height; var i = x.getDimensions(this.el), a = e.chart.width.toString().split(/[0-9]+/g).pop(); "%" === a ? x.isNumber(i[0]) && (0 === i[0].width && (i = x.getDimensions(this.el.parentNode)), t.svgWidth = i[0] * parseInt(e.chart.width, 10) / 100) : "px" !== a && "" !== a || (t.svgWidth = parseInt(e.chart.width, 10)); var s = e.chart.height.toString().split(/[0-9]+/g).pop(); if ("auto" !== t.svgHeight && "" !== t.svgHeight) if ("%" === s) { var r = x.getDimensions(this.el.parentNode); t.svgHeight = r[1] * parseInt(e.chart.height, 10) / 100 } else t.svgHeight = parseInt(e.chart.height, 10); else t.axisCharts ? t.svgHeight = t.svgWidth / 1.61 : t.svgHeight = t.svgWidth / 1.2; if (t.svgWidth < 0 && (t.svgWidth = 0), t.svgHeight < 0 && (t.svgHeight = 0), m.setAttrs(t.dom.Paper.node, { width: t.svgWidth, height: t.svgHeight }), "%" !== s) { var o = e.chart.sparkline.enabled ? 0 : t.axisCharts ? e.chart.parentHeightOffset : 0; t.dom.Paper.node.parentNode.parentNode.style.minHeight = t.svgHeight + o + "px" } t.dom.elWrap.style.width = t.svgWidth + "px", t.dom.elWrap.style.height = t.svgHeight + "px" } }, { key: "shiftGraphPosition", value: function () { var t = this.w.globals, e = t.translateY, i = { transform: "translate(" + t.translateX + ", " + e + ")" }; m.setAttrs(t.dom.elGraphical.node, i) } }, { key: "resizeNonAxisCharts", value: function () { var t = this.w, e = t.globals, i = 0, a = t.config.chart.sparkline.enabled ? 1 : 15; a += t.config.grid.padding.bottom, "top" !== t.config.legend.position && "bottom" !== t.config.legend.position || !t.config.legend.show || t.config.legend.floating || (i = new lt(this.ctx).legendHelpers.getLegendBBox().clwh + 10); var s = t.globals.dom.baseEl.querySelector(".apexcharts-radialbar, .apexcharts-pie"), r = 2.05 * t.globals.radialSize; if (s && !t.config.chart.sparkline.enabled && 0 !== t.config.plotOptions.radialBar.startAngle) { var o = x.getBoundingClientRect(s); r = o.bottom; var n = o.bottom - o.top; r = Math.max(2.05 * t.globals.radialSize, n) } var l = r + e.translateY + i + a; e.dom.elLegendForeign && e.dom.elLegendForeign.setAttribute("height", l), t.config.chart.height && String(t.config.chart.height).indexOf("%") > 0 || (e.dom.elWrap.style.height = l + "px", m.setAttrs(e.dom.Paper.node, { height: l }), e.dom.Paper.node.parentNode.parentNode.style.minHeight = l + "px") } }, { key: "coreCalculations", value: function () { new U(this.ctx).init() } }, { key: "resetGlobals", value: function () { var t = this, e = function () { return t.w.config.series.map((function (t) { return [] })) }, i = new F, a = this.w.globals; i.initGlobalVars(a), a.seriesXvalues = e(), a.seriesYvalues = e() } }, { key: "isMultipleY", value: function () { if (this.w.config.yaxis.constructor === Array && this.w.config.yaxis.length > 1) return this.w.globals.isMultipleYAxis = !0, !0 } }, { key: "xySettings", value: function () { var t = null, e = this.w; if (e.globals.axisCharts) { if ("back" === e.config.xaxis.crosshairs.position) new Q(this.ctx).drawXCrosshairs(); if ("back" === e.config.yaxis[0].crosshairs.position) new Q(this.ctx).drawYCrosshairs(); if ("datetime" === e.config.xaxis.type && void 0 === e.config.xaxis.labels.formatter) { this.ctx.timeScale = new Nt(this.ctx); var i = []; isFinite(e.globals.minX) && isFinite(e.globals.maxX) && !e.globals.isBarHorizontal ? i = this.ctx.timeScale.calculateTimeScaleTicks(e.globals.minX, e.globals.maxX) : e.globals.isBarHorizontal && (i = this.ctx.timeScale.calculateTimeScaleTicks(e.globals.minY, e.globals.maxY)), this.ctx.timeScale.recalcDimensionsBasedOnFormat(i) } t = new y(this.ctx).getCalculatedRatios() } return t } }, { key: "updateSourceChart", value: function (t) { this.ctx.w.globals.selection = void 0, this.ctx.updateHelpers._updateOptions({ chart: { selection: { xaxis: { min: t.w.globals.minX, max: t.w.globals.maxX } } } }, !1, !1) } }, { key: "setupBrushHandler", value: function () { var t = this, e = this.w; if (e.config.chart.brush.enabled && "function" != typeof e.config.chart.events.selection) { var i = Array.isArray(e.config.chart.brush.targets) ? e.config.chart.brush.targets : [e.config.chart.brush.target]; i.forEach((function (e) { var i = ApexCharts.getChartByID(e); i.w.globals.brushSource = t.ctx, "function" != typeof i.w.config.chart.events.zoomed && (i.w.config.chart.events.zoomed = function () { t.updateSourceChart(i) }), "function" != typeof i.w.config.chart.events.scrolled && (i.w.config.chart.events.scrolled = function () { t.updateSourceChart(i) }) })), e.config.chart.events.selection = function (t, e) { i.forEach((function (t) { ApexCharts.getChartByID(t).ctx.updateHelpers._updateOptions({ xaxis: { min: e.xaxis.min, max: e.xaxis.max } }, !1, !1, !1, !1) })) } } } }]), t }(), Bt = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w } return r(t, [{ key: "_updateOptions", value: function (t) { var e = this, a = arguments.length > 1 && void 0 !== arguments[1] && arguments[1], s = !(arguments.length > 2 && void 0 !== arguments[2]) || arguments[2], r = !(arguments.length > 3 && void 0 !== arguments[3]) || arguments[3], o = arguments.length > 4 && void 0 !== arguments[4] && arguments[4]; return new Promise((function (n) { var l = [e.ctx]; r && (l = e.ctx.getSyncedCharts()), e.ctx.w.globals.isExecCalled && (l = [e.ctx], e.ctx.w.globals.isExecCalled = !1), l.forEach((function (r, h) { var c = r.w; if (c.globals.shouldAnimate = s, a || (c.globals.resized = !0, c.globals.dataChanged = !0, s && r.series.getPreviousPaths()), t && "object" === i(t) && (r.config = new Y(t), t = y.extendArrayProps(r.config, t, c), r.w.globals.chartID !== e.ctx.w.globals.chartID && delete t.series, c.config = x.extend(c.config, t), o && (c.globals.lastXAxis = t.xaxis ? x.clone(t.xaxis) : [], c.globals.lastYAxis = t.yaxis ? x.clone(t.yaxis) : [], c.globals.initialConfig = x.extend({}, c.config), c.globals.initialSeries = x.clone(c.config.series), t.series))) { for (var d = 0; d < c.globals.collapsedSeriesIndices.length; d++) { var g = c.config.series[c.globals.collapsedSeriesIndices[d]]; c.globals.collapsedSeries[d].data = c.globals.axisCharts ? g.data.slice() : g } for (var u = 0; u < c.globals.ancillaryCollapsedSeriesIndices.length; u++) { var p = c.config.series[c.globals.ancillaryCollapsedSeriesIndices[u]]; c.globals.ancillaryCollapsedSeries[u].data = c.globals.axisCharts ? p.data.slice() : p } r.series.emptyCollapsedSeries(c.config.series) } return r.update(t).then((function () { h === l.length - 1 && n(r) })) })) })) } }, { key: "_updateSeries", value: function (t, e) { var i = this, a = arguments.length > 2 && void 0 !== arguments[2] && arguments[2]; return new Promise((function (s) { var r, o = i.w; return o.globals.shouldAnimate = e, o.globals.dataChanged = !0, e && i.ctx.series.getPreviousPaths(), o.globals.axisCharts ? (0 === (r = t.map((function (t, e) { return i._extendSeries(t, e) }))).length && (r = [{ data: [] }]), o.config.series = r) : o.config.series = t.slice(), a && (o.globals.initialConfig.series = x.clone(o.config.series), o.globals.initialSeries = x.clone(o.config.series)), i.ctx.update().then((function () { s(i.ctx) })) })) } }, { key: "_extendSeries", value: function (t, i) { var a = this.w, s = a.config.series[i]; return e(e({}, a.config.series[i]), {}, { name: t.name ? t.name : null == s ? void 0 : s.name, color: t.color ? t.color : null == s ? void 0 : s.color, type: t.type ? t.type : null == s ? void 0 : s.type, group: t.group ? t.group : null == s ? void 0 : s.group, data: t.data ? t.data : null == s ? void 0 : s.data, zIndex: void 0 !== t.zIndex ? t.zIndex : i }) } }, { key: "toggleDataPointSelection", value: function (t, e) { var i = this.w, a = null, s = ".apexcharts-series[data\\:realIndex='".concat(t, "']"); return i.globals.axisCharts ? a = i.globals.dom.Paper.select("".concat(s, " path[j='").concat(e, "'], ").concat(s, " circle[j='").concat(e, "'], ").concat(s, " rect[j='").concat(e, "']")).members[0] : void 0 === e && (a = i.globals.dom.Paper.select("".concat(s, " path[j='").concat(t, "']")).members[0], "pie" !== i.config.chart.type && "polarArea" !== i.config.chart.type && "donut" !== i.config.chart.type || this.ctx.pie.pieClicked(t)), a ? (new m(this.ctx).pathMouseDown(a, null), a.node ? a.node : null) : (console.warn("toggleDataPointSelection: Element not found"), null) } }, { key: "forceXAxisUpdate", value: function (t) { var e = this.w; if (["min", "max"].forEach((function (i) { void 0 !== t.xaxis[i] && (e.config.xaxis[i] = t.xaxis[i], e.globals.lastXAxis[i] = t.xaxis[i]) })), t.xaxis.categories && t.xaxis.categories.length && (e.config.xaxis.categories = t.xaxis.categories), e.config.xaxis.convertedCatToNumeric) { var i = new E(t); t = i.convertCatToNumericXaxis(t, this.ctx) } return t } }, { key: "forceYAxisUpdate", value: function (t) { return t.chart && t.chart.stacked && "100%" === t.chart.stackType && (Array.isArray(t.yaxis) ? t.yaxis.forEach((function (e, i) { t.yaxis[i].min = 0, t.yaxis[i].max = 100 })) : (t.yaxis.min = 0, t.yaxis.max = 100)), t } }, { key: "revertDefaultAxisMinMax", value: function (t) { var e = this, i = this.w, a = i.globals.lastXAxis, s = i.globals.lastYAxis; t && t.xaxis && (a = t.xaxis), t && t.yaxis && (s = t.yaxis), i.config.xaxis.min = a.min, i.config.xaxis.max = a.max; var r = function (t) { void 0 !== s[t] && (i.config.yaxis[t].min = s[t].min, i.config.yaxis[t].max = s[t].max) }; i.config.yaxis.map((function (t, a) { i.globals.zoomed || void 0 !== s[a] ? r(a) : void 0 !== e.ctx.opts.yaxis[a] && (t.min = e.ctx.opts.yaxis[a].min, t.max = e.ctx.opts.yaxis[a].max) })) } }]), t }(); Rt = "undefined" != typeof window ? window : void 0, Ht = function (t, e) { var a = (void 0 !== this ? this : t).SVG = function (t) { if (a.supported) return t = new a.Doc(t), a.parser.draw || a.prepare(), t }; if (a.ns = "http://www.w3.org/2000/svg", a.xmlns = "http://www.w3.org/2000/xmlns/", a.xlink = "http://www.w3.org/1999/xlink", a.svgjs = "http://svgjs.dev", a.supported = !0, !a.supported) return !1; a.did = 1e3, a.eid = function (t) { return "Svgjs" + d(t) + a.did++ }, a.create = function (t) { var i = e.createElementNS(this.ns, t); return i.setAttribute("id", this.eid(t)), i }, a.extend = function () { var t, e; e = (t = [].slice.call(arguments)).pop(); for (var i = t.length - 1; i >= 0; i--)if (t[i]) for (var s in e) t[i].prototype[s] = e[s]; a.Set && a.Set.inherit && a.Set.inherit() }, a.invent = function (t) { var e = "function" == typeof t.create ? t.create : function () { this.constructor.call(this, a.create(t.create)) }; return t.inherit && (e.prototype = new t.inherit), t.extend && a.extend(e, t.extend), t.construct && a.extend(t.parent || a.Container, t.construct), e }, a.adopt = function (e) { return e ? e.instance ? e.instance : ((i = "svg" == e.nodeName ? e.parentNode instanceof t.SVGElement ? new a.Nested : new a.Doc : "linearGradient" == e.nodeName ? new a.Gradient("linear") : "radialGradient" == e.nodeName ? new a.Gradient("radial") : a[d(e.nodeName)] ? new (a[d(e.nodeName)]) : new a.Element(e)).type = e.nodeName, i.node = e, e.instance = i, i instanceof a.Doc && i.namespace().defs(), i.setData(JSON.parse(e.getAttribute("svgjs:data")) || {}), i) : null; var i }, a.prepare = function () { var t = e.getElementsByTagName("body")[0], i = (t ? new a.Doc(t) : a.adopt(e.documentElement).nested()).size(2, 0); a.parser = { body: t || e.documentElement, draw: i.style("opacity:0;position:absolute;left:-100%;top:-100%;overflow:hidden").node, poly: i.polyline().node, path: i.path().node, native: a.create("svg") } }, a.parser = { native: a.create("svg") }, e.addEventListener("DOMContentLoaded", (function () { a.parser.draw || a.prepare() }), !1), a.regex = { numberAndUnit: /^([+-]?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?)([a-z%]*)$/i, hex: /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i, rgb: /rgb\((\d+),(\d+),(\d+)\)/, reference: /#([a-z0-9\-_]+)/i, transforms: /\)\s*,?\s*/, whitespace: /\s/g, isHex: /^#[a-f0-9]{3,6}$/i, isRgb: /^rgb\(/, isCss: /[^:]+:[^;]+;?/, isBlank: /^(\s+)?$/, isNumber: /^[+-]?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i, isPercent: /^-?[\d\.]+%$/, isImage: /\.(jpg|jpeg|png|gif|svg)(\?[^=]+.*)?/i, delimiter: /[\s,]+/, hyphen: /([^e])\-/gi, pathLetters: /[MLHVCSQTAZ]/gi, isPathLetter: /[MLHVCSQTAZ]/i, numbersWithDots: /((\d?\.\d+(?:e[+-]?\d+)?)((?:\.\d+(?:e[+-]?\d+)?)+))+/gi, dots: /\./g }, a.utils = { map: function (t, e) { for (var i = t.length, a = [], s = 0; s < i; s++)a.push(e(t[s])); return a }, filter: function (t, e) { for (var i = t.length, a = [], s = 0; s < i; s++)e(t[s]) && a.push(t[s]); return a }, filterSVGElements: function (e) { return this.filter(e, (function (e) { return e instanceof t.SVGElement })) } }, a.defaults = { attrs: { "fill-opacity": 1, "stroke-opacity": 1, "stroke-width": 0, "stroke-linejoin": "miter", "stroke-linecap": "butt", fill: "#000000", stroke: "#000000", opacity: 1, x: 0, y: 0, cx: 0, cy: 0, width: 0, height: 0, r: 0, rx: 0, ry: 0, offset: 0, "stop-opacity": 1, "stop-color": "#000000", "font-size": 16, "font-family": "Helvetica, Arial, sans-serif", "text-anchor": "start" } }, a.Color = function (t) { var e, s; this.r = 0, this.g = 0, this.b = 0, t && ("string" == typeof t ? a.regex.isRgb.test(t) ? (e = a.regex.rgb.exec(t.replace(a.regex.whitespace, "")), this.r = parseInt(e[1]), this.g = parseInt(e[2]), this.b = parseInt(e[3])) : a.regex.isHex.test(t) && (e = a.regex.hex.exec(4 == (s = t).length ? ["#", s.substring(1, 2), s.substring(1, 2), s.substring(2, 3), s.substring(2, 3), s.substring(3, 4), s.substring(3, 4)].join("") : s), this.r = parseInt(e[1], 16), this.g = parseInt(e[2], 16), this.b = parseInt(e[3], 16)) : "object" === i(t) && (this.r = t.r, this.g = t.g, this.b = t.b)) }, a.extend(a.Color, { toString: function () { return this.toHex() }, toHex: function () { return "#" + g(this.r) + g(this.g) + g(this.b) }, toRgb: function () { return "rgb(" + [this.r, this.g, this.b].join() + ")" }, brightness: function () { return this.r / 255 * .3 + this.g / 255 * .59 + this.b / 255 * .11 }, morph: function (t) { return this.destination = new a.Color(t), this }, at: function (t) { return this.destination ? (t = t < 0 ? 0 : t > 1 ? 1 : t, new a.Color({ r: ~~(this.r + (this.destination.r - this.r) * t), g: ~~(this.g + (this.destination.g - this.g) * t), b: ~~(this.b + (this.destination.b - this.b) * t) })) : this } }), a.Color.test = function (t) { return t += "", a.regex.isHex.test(t) || a.regex.isRgb.test(t) }, a.Color.isRgb = function (t) { return t && "number" == typeof t.r && "number" == typeof t.g && "number" == typeof t.b }, a.Color.isColor = function (t) { return a.Color.isRgb(t) || a.Color.test(t) }, a.Array = function (t, e) { 0 == (t = (t || []).valueOf()).length && e && (t = e.valueOf()), this.value = this.parse(t) }, a.extend(a.Array, { toString: function () { return this.value.join(" ") }, valueOf: function () { return this.value }, parse: function (t) { return t = t.valueOf(), Array.isArray(t) ? t : this.split(t) } }), a.PointArray = function (t, e) { a.Array.call(this, t, e || [[0, 0]]) }, a.PointArray.prototype = new a.Array, a.PointArray.prototype.constructor = a.PointArray; for (var s = { M: function (t, e, i) { return e.x = i.x = t[0], e.y = i.y = t[1], ["M", e.x, e.y] }, L: function (t, e) { return e.x = t[0], e.y = t[1], ["L", t[0], t[1]] }, H: function (t, e) { return e.x = t[0], ["H", t[0]] }, V: function (t, e) { return e.y = t[0], ["V", t[0]] }, C: function (t, e) { return e.x = t[4], e.y = t[5], ["C", t[0], t[1], t[2], t[3], t[4], t[5]] }, Q: function (t, e) { return e.x = t[2], e.y = t[3], ["Q", t[0], t[1], t[2], t[3]] }, S: function (t, e) { return e.x = t[2], e.y = t[3], ["S", t[0], t[1], t[2], t[3]] }, Z: function (t, e, i) { return e.x = i.x, e.y = i.y, ["Z"] } }, r = "mlhvqtcsaz".split(""), o = 0, n = r.length; o < n; ++o)s[r[o]] = function (t) { return function (e, i, a) { if ("H" == t) e[0] = e[0] + i.x; else if ("V" == t) e[0] = e[0] + i.y; else if ("A" == t) e[5] = e[5] + i.x, e[6] = e[6] + i.y; else for (var r = 0, o = e.length; r < o; ++r)e[r] = e[r] + (r % 2 ? i.y : i.x); if (s && "function" == typeof s[t]) return s[t](e, i, a) } }(r[o].toUpperCase()); a.PathArray = function (t, e) { a.Array.call(this, t, e || [["M", 0, 0]]) }, a.PathArray.prototype = new a.Array, a.PathArray.prototype.constructor = a.PathArray, a.extend(a.PathArray, { toString: function () { return function (t) { for (var e = 0, i = t.length, a = ""; e < i; e++)a += t[e][0], null != t[e][1] && (a += t[e][1], null != t[e][2] && (a += " ", a += t[e][2], null != t[e][3] && (a += " ", a += t[e][3], a += " ", a += t[e][4], null != t[e][5] && (a += " ", a += t[e][5], a += " ", a += t[e][6], null != t[e][7] && (a += " ", a += t[e][7]))))); return a + " " }(this.value) }, move: function (t, e) { var i = this.bbox(); return i.x, i.y, this }, at: function (t) { if (!this.destination) return this; for (var e = this.value, i = this.destination.value, s = [], r = new a.PathArray, o = 0, n = e.length; o < n; o++) { s[o] = [e[o][0]]; for (var l = 1, h = e[o].length; l < h; l++)s[o][l] = e[o][l] + (i[o][l] - e[o][l]) * t; "A" === s[o][0] && (s[o][4] = +(0 != s[o][4]), s[o][5] = +(0 != s[o][5])) } return r.value = s, r }, parse: function (t) { if (t instanceof a.PathArray) return t.valueOf(); var e, i = { M: 2, L: 2, H: 1, V: 1, C: 6, S: 4, Q: 4, T: 2, A: 7, Z: 0 }; t = "string" == typeof t ? t.replace(a.regex.numbersWithDots, h).replace(a.regex.pathLetters, " $& ").replace(a.regex.hyphen, "$1 -").trim().split(a.regex.delimiter) : t.reduce((function (t, e) { return [].concat.call(t, e) }), []); var r = [], o = new a.Point, n = new a.Point, l = 0, c = t.length; do { a.regex.isPathLetter.test(t[l]) ? (e = t[l], ++l) : "M" == e ? e = "L" : "m" == e && (e = "l"), r.push(s[e].call(null, t.slice(l, l += i[e.toUpperCase()]).map(parseFloat), o, n)) } while (c > l); return r }, bbox: function () { return a.parser.draw || a.prepare(), a.parser.path.setAttribute("d", this.toString()), a.parser.path.getBBox() } }), a.Number = a.invent({ create: function (t, e) { this.value = 0, this.unit = e || "", "number" == typeof t ? this.value = isNaN(t) ? 0 : isFinite(t) ? t : t < 0 ? -34e37 : 34e37 : "string" == typeof t ? (e = t.match(a.regex.numberAndUnit)) && (this.value = parseFloat(e[1]), "%" == e[5] ? this.value /= 100 : "s" == e[5] && (this.value *= 1e3), this.unit = e[5]) : t instanceof a.Number && (this.value = t.valueOf(), this.unit = t.unit) }, extend: { toString: function () { return ("%" == this.unit ? ~~(1e8 * this.value) / 1e6 : "s" == this.unit ? this.value / 1e3 : this.value) + this.unit }, toJSON: function () { return this.toString() }, valueOf: function () { return this.value }, plus: function (t) { return t = new a.Number(t), new a.Number(this + t, this.unit || t.unit) }, minus: function (t) { return t = new a.Number(t), new a.Number(this - t, this.unit || t.unit) }, times: function (t) { return t = new a.Number(t), new a.Number(this * t, this.unit || t.unit) }, divide: function (t) { return t = new a.Number(t), new a.Number(this / t, this.unit || t.unit) }, to: function (t) { var e = new a.Number(this); return "string" == typeof t && (e.unit = t), e }, morph: function (t) { return this.destination = new a.Number(t), t.relative && (this.destination.value += this.value), this }, at: function (t) { return this.destination ? new a.Number(this.destination).minus(this).times(t).plus(this) : this } } }), a.Element = a.invent({ create: function (t) { this._stroke = a.defaults.attrs.stroke, this._event = null, this.dom = {}, (this.node = t) && (this.type = t.nodeName, this.node.instance = this, this._stroke = t.getAttribute("stroke") || this._stroke) }, extend: { x: function (t) { return this.attr("x", t) }, y: function (t) { return this.attr("y", t) }, cx: function (t) { return null == t ? this.x() + this.width() / 2 : this.x(t - this.width() / 2) }, cy: function (t) { return null == t ? this.y() + this.height() / 2 : this.y(t - this.height() / 2) }, move: function (t, e) { return this.x(t).y(e) }, center: function (t, e) { return this.cx(t).cy(e) }, width: function (t) { return this.attr("width", t) }, height: function (t) { return this.attr("height", t) }, size: function (t, e) { var i = u(this, t, e); return this.width(new a.Number(i.width)).height(new a.Number(i.height)) }, clone: function (t) { this.writeDataToDom(); var e = x(this.node.cloneNode(!0)); return t ? t.add(e) : this.after(e), e }, remove: function () { return this.parent() && this.parent().removeElement(this), this }, replace: function (t) { return this.after(t).remove(), t }, addTo: function (t) { return t.put(this) }, putIn: function (t) { return t.add(this) }, id: function (t) { return this.attr("id", t) }, show: function () { return this.style("display", "") }, hide: function () { return this.style("display", "none") }, visible: function () { return "none" != this.style("display") }, toString: function () { return this.attr("id") }, classes: function () { var t = this.attr("class"); return null == t ? [] : t.trim().split(a.regex.delimiter) }, hasClass: function (t) { return -1 != this.classes().indexOf(t) }, addClass: function (t) { if (!this.hasClass(t)) { var e = this.classes(); e.push(t), this.attr("class", e.join(" ")) } return this }, removeClass: function (t) { return this.hasClass(t) && this.attr("class", this.classes().filter((function (e) { return e != t })).join(" ")), this }, toggleClass: function (t) { return this.hasClass(t) ? this.removeClass(t) : this.addClass(t) }, reference: function (t) { return a.get(this.attr(t)) }, parent: function (e) { var i = this; if (!i.node.parentNode) return null; if (i = a.adopt(i.node.parentNode), !e) return i; for (; i && i.node instanceof t.SVGElement;) { if ("string" == typeof e ? i.matches(e) : i instanceof e) return i; if (!i.node.parentNode || "#document" == i.node.parentNode.nodeName) return null; i = a.adopt(i.node.parentNode) } }, doc: function () { return this instanceof a.Doc ? this : this.parent(a.Doc) }, parents: function (t) { var e = [], i = this; do { if (!(i = i.parent(t)) || !i.node) break; e.push(i) } while (i.parent); return e }, matches: function (t) { return function (t, e) { return (t.matches || t.matchesSelector || t.msMatchesSelector || t.mozMatchesSelector || t.webkitMatchesSelector || t.oMatchesSelector).call(t, e) }(this.node, t) }, native: function () { return this.node }, svg: function (t) { var i = e.createElementNS("http://www.w3.org/2000/svg", "svg"); if (!(t && this instanceof a.Parent)) return i.appendChild(t = e.createElementNS("http://www.w3.org/2000/svg", "svg")), this.writeDataToDom(), t.appendChild(this.node.cloneNode(!0)), i.innerHTML.replace(/^/, "").replace(/<\/svg>$/, ""); i.innerHTML = "" + t.replace(/\n/, "").replace(/<([\w:-]+)([^<]+?)\/>/g, "<$1$2>") + ""; for (var s = 0, r = i.firstChild.childNodes.length; s < r; s++)this.node.appendChild(i.firstChild.firstChild); return this }, writeDataToDom: function () { return (this.each || this.lines) && (this.each ? this : this.lines()).each((function () { this.writeDataToDom() })), this.node.removeAttribute("svgjs:data"), Object.keys(this.dom).length && this.node.setAttribute("svgjs:data", JSON.stringify(this.dom)), this }, setData: function (t) { return this.dom = t, this }, is: function (t) { return function (t, e) { return t instanceof e }(this, t) } } }), a.easing = { "-": function (t) { return t }, "<>": function (t) { return -Math.cos(t * Math.PI) / 2 + .5 }, ">": function (t) { return Math.sin(t * Math.PI / 2) }, "<": function (t) { return 1 - Math.cos(t * Math.PI / 2) } }, a.morph = function (t) { return function (e, i) { return new a.MorphObj(e, i).at(t) } }, a.Situation = a.invent({ create: function (t) { this.init = !1, this.reversed = !1, this.reversing = !1, this.duration = new a.Number(t.duration).valueOf(), this.delay = new a.Number(t.delay).valueOf(), this.start = +new Date + this.delay, this.finish = this.start + this.duration, this.ease = t.ease, this.loop = 0, this.loops = !1, this.animations = {}, this.attrs = {}, this.styles = {}, this.transforms = [], this.once = {} } }), a.FX = a.invent({ create: function (t) { this._target = t, this.situations = [], this.active = !1, this.situation = null, this.paused = !1, this.lastPos = 0, this.pos = 0, this.absPos = 0, this._speed = 1 }, extend: { animate: function (t, e, s) { "object" === i(t) && (e = t.ease, s = t.delay, t = t.duration); var r = new a.Situation({ duration: t || 1e3, delay: s || 0, ease: a.easing[e || "-"] || e }); return this.queue(r), this }, target: function (t) { return t && t instanceof a.Element ? (this._target = t, this) : this._target }, timeToAbsPos: function (t) { return (t - this.situation.start) / (this.situation.duration / this._speed) }, absPosToTime: function (t) { return this.situation.duration / this._speed * t + this.situation.start }, startAnimFrame: function () { this.stopAnimFrame(), this.animationFrame = t.requestAnimationFrame(function () { this.step() }.bind(this)) }, stopAnimFrame: function () { t.cancelAnimationFrame(this.animationFrame) }, start: function () { return !this.active && this.situation && (this.active = !0, this.startCurrent()), this }, startCurrent: function () { return this.situation.start = +new Date + this.situation.delay / this._speed, this.situation.finish = this.situation.start + this.situation.duration / this._speed, this.initAnimations().step() }, queue: function (t) { return ("function" == typeof t || t instanceof a.Situation) && this.situations.push(t), this.situation || (this.situation = this.situations.shift()), this }, dequeue: function () { return this.stop(), this.situation = this.situations.shift(), this.situation && (this.situation instanceof a.Situation ? this.start() : this.situation.call(this)), this }, initAnimations: function () { var t, e = this.situation; if (e.init) return this; for (var i in e.animations) { t = this.target()[i](), Array.isArray(t) || (t = [t]), Array.isArray(e.animations[i]) || (e.animations[i] = [e.animations[i]]); for (var s = t.length; s--;)e.animations[i][s] instanceof a.Number && (t[s] = new a.Number(t[s])), e.animations[i][s] = t[s].morph(e.animations[i][s]) } for (var i in e.attrs) e.attrs[i] = new a.MorphObj(this.target().attr(i), e.attrs[i]); for (var i in e.styles) e.styles[i] = new a.MorphObj(this.target().style(i), e.styles[i]); return e.initialTransformation = this.target().matrixify(), e.init = !0, this }, clearQueue: function () { return this.situations = [], this }, clearCurrent: function () { return this.situation = null, this }, stop: function (t, e) { var i = this.active; return this.active = !1, e && this.clearQueue(), t && this.situation && (!i && this.startCurrent(), this.atEnd()), this.stopAnimFrame(), this.clearCurrent() }, after: function (t) { var e = this.last(); return this.target().on("finished.fx", (function i(a) { a.detail.situation == e && (t.call(this, e), this.off("finished.fx", i)) })), this._callStart() }, during: function (t) { var e = this.last(), i = function (i) { i.detail.situation == e && t.call(this, i.detail.pos, a.morph(i.detail.pos), i.detail.eased, e) }; return this.target().off("during.fx", i).on("during.fx", i), this.after((function () { this.off("during.fx", i) })), this._callStart() }, afterAll: function (t) { var e = function e(i) { t.call(this), this.off("allfinished.fx", e) }; return this.target().off("allfinished.fx", e).on("allfinished.fx", e), this._callStart() }, last: function () { return this.situations.length ? this.situations[this.situations.length - 1] : this.situation }, add: function (t, e, i) { return this.last()[i || "animations"][t] = e, this._callStart() }, step: function (t) { var e, i, a; t || (this.absPos = this.timeToAbsPos(+new Date)), !1 !== this.situation.loops ? (e = Math.max(this.absPos, 0), i = Math.floor(e), !0 === this.situation.loops || i < this.situation.loops ? (this.pos = e - i, a = this.situation.loop, this.situation.loop = i) : (this.absPos = this.situation.loops, this.pos = 1, a = this.situation.loop - 1, this.situation.loop = this.situation.loops), this.situation.reversing && (this.situation.reversed = this.situation.reversed != Boolean((this.situation.loop - a) % 2))) : (this.absPos = Math.min(this.absPos, 1), this.pos = this.absPos), this.pos < 0 && (this.pos = 0), this.situation.reversed && (this.pos = 1 - this.pos); var s = this.situation.ease(this.pos); for (var r in this.situation.once) r > this.lastPos && r <= s && (this.situation.once[r].call(this.target(), this.pos, s), delete this.situation.once[r]); return this.active && this.target().fire("during", { pos: this.pos, eased: s, fx: this, situation: this.situation }), this.situation ? (this.eachAt(), 1 == this.pos && !this.situation.reversed || this.situation.reversed && 0 == this.pos ? (this.stopAnimFrame(), this.target().fire("finished", { fx: this, situation: this.situation }), this.situations.length || (this.target().fire("allfinished"), this.situations.length || (this.target().off(".fx"), this.active = !1)), this.active ? this.dequeue() : this.clearCurrent()) : !this.paused && this.active && this.startAnimFrame(), this.lastPos = s, this) : this }, eachAt: function () { var t, e = this, i = this.target(), s = this.situation; for (var r in s.animations) t = [].concat(s.animations[r]).map((function (t) { return "string" != typeof t && t.at ? t.at(s.ease(e.pos), e.pos) : t })), i[r].apply(i, t); for (var r in s.attrs) t = [r].concat(s.attrs[r]).map((function (t) { return "string" != typeof t && t.at ? t.at(s.ease(e.pos), e.pos) : t })), i.attr.apply(i, t); for (var r in s.styles) t = [r].concat(s.styles[r]).map((function (t) { return "string" != typeof t && t.at ? t.at(s.ease(e.pos), e.pos) : t })), i.style.apply(i, t); if (s.transforms.length) { t = s.initialTransformation, r = 0; for (var o = s.transforms.length; r < o; r++) { var n = s.transforms[r]; n instanceof a.Matrix ? t = n.relative ? t.multiply((new a.Matrix).morph(n).at(s.ease(this.pos))) : t.morph(n).at(s.ease(this.pos)) : (n.relative || n.undo(t.extract()), t = t.multiply(n.at(s.ease(this.pos)))) } i.matrix(t) } return this }, once: function (t, e, i) { var a = this.last(); return i || (t = a.ease(t)), a.once[t] = e, this }, _callStart: function () { return setTimeout(function () { this.start() }.bind(this), 0), this } }, parent: a.Element, construct: { animate: function (t, e, i) { return (this.fx || (this.fx = new a.FX(this))).animate(t, e, i) }, delay: function (t) { return (this.fx || (this.fx = new a.FX(this))).delay(t) }, stop: function (t, e) { return this.fx && this.fx.stop(t, e), this }, finish: function () { return this.fx && this.fx.finish(), this } } }), a.MorphObj = a.invent({ create: function (t, e) { return a.Color.isColor(e) ? new a.Color(t).morph(e) : a.regex.delimiter.test(t) ? a.regex.pathLetters.test(t) ? new a.PathArray(t).morph(e) : new a.Array(t).morph(e) : a.regex.numberAndUnit.test(e) ? new a.Number(t).morph(e) : (this.value = t, void (this.destination = e)) }, extend: { at: function (t, e) { return e < 1 ? this.value : this.destination }, valueOf: function () { return this.value } } }), a.extend(a.FX, { attr: function (t, e, a) { if ("object" === i(t)) for (var s in t) this.attr(s, t[s]); else this.add(t, e, "attrs"); return this }, plot: function (t, e, i, a) { return 4 == arguments.length ? this.plot([t, e, i, a]) : this.add("plot", new (this.target().morphArray)(t)) } }), a.Box = a.invent({ create: function (t, e, s, r) { if (!("object" !== i(t) || t instanceof a.Element)) return a.Box.call(this, null != t.left ? t.left : t.x, null != t.top ? t.top : t.y, t.width, t.height); var o; 4 == arguments.length && (this.x = t, this.y = e, this.width = s, this.height = r), null == (o = this).x && (o.x = 0, o.y = 0, o.width = 0, o.height = 0), o.w = o.width, o.h = o.height, o.x2 = o.x + o.width, o.y2 = o.y + o.height, o.cx = o.x + o.width / 2, o.cy = o.y + o.height / 2 } }), a.BBox = a.invent({ create: function (t) { if (a.Box.apply(this, [].slice.call(arguments)), t instanceof a.Element) { var i; try { if (!e.documentElement.contains) { for (var s = t.node; s.parentNode;)s = s.parentNode; if (s != e) throw new Error("Element not in the dom") } i = t.node.getBBox() } catch (e) { if (t instanceof a.Shape) { a.parser.draw || a.prepare(); var r = t.clone(a.parser.draw.instance).show(); r && r.node && "function" == typeof r.node.getBBox && (i = r.node.getBBox()), r && "function" == typeof r.remove && r.remove() } else i = { x: t.node.clientLeft, y: t.node.clientTop, width: t.node.clientWidth, height: t.node.clientHeight } } a.Box.call(this, i) } }, inherit: a.Box, parent: a.Element, construct: { bbox: function () { return new a.BBox(this) } } }), a.BBox.prototype.constructor = a.BBox, a.Matrix = a.invent({ create: function (t) { var e = f([1, 0, 0, 1, 0, 0]); t = null === t ? e : t instanceof a.Element ? t.matrixify() : "string" == typeof t ? f(t.split(a.regex.delimiter).map(parseFloat)) : 6 == arguments.length ? f([].slice.call(arguments)) : Array.isArray(t) ? f(t) : t && "object" === i(t) ? t : e; for (var s = v.length - 1; s >= 0; --s)this[v[s]] = null != t[v[s]] ? t[v[s]] : e[v[s]] }, extend: { extract: function () { var t = p(this, 0, 1); p(this, 1, 0); var e = 180 / Math.PI * Math.atan2(t.y, t.x) - 90; return { x: this.e, y: this.f, transformedX: (this.e * Math.cos(e * Math.PI / 180) + this.f * Math.sin(e * Math.PI / 180)) / Math.sqrt(this.a * this.a + this.b * this.b), transformedY: (this.f * Math.cos(e * Math.PI / 180) + this.e * Math.sin(-e * Math.PI / 180)) / Math.sqrt(this.c * this.c + this.d * this.d), rotation: e, a: this.a, b: this.b, c: this.c, d: this.d, e: this.e, f: this.f, matrix: new a.Matrix(this) } }, clone: function () { return new a.Matrix(this) }, morph: function (t) { return this.destination = new a.Matrix(t), this }, multiply: function (t) { return new a.Matrix(this.native().multiply(function (t) { return t instanceof a.Matrix || (t = new a.Matrix(t)), t }(t).native())) }, inverse: function () { return new a.Matrix(this.native().inverse()) }, translate: function (t, e) { return new a.Matrix(this.native().translate(t || 0, e || 0)) }, native: function () { for (var t = a.parser.native.createSVGMatrix(), e = v.length - 1; e >= 0; e--)t[v[e]] = this[v[e]]; return t }, toString: function () { return "matrix(" + b(this.a) + "," + b(this.b) + "," + b(this.c) + "," + b(this.d) + "," + b(this.e) + "," + b(this.f) + ")" } }, parent: a.Element, construct: { ctm: function () { return new a.Matrix(this.node.getCTM()) }, screenCTM: function () { if (this instanceof a.Nested) { var t = this.rect(1, 1), e = t.node.getScreenCTM(); return t.remove(), new a.Matrix(e) } return new a.Matrix(this.node.getScreenCTM()) } } }), a.Point = a.invent({ create: function (t, e) { var a; a = Array.isArray(t) ? { x: t[0], y: t[1] } : "object" === i(t) ? { x: t.x, y: t.y } : null != t ? { x: t, y: null != e ? e : t } : { x: 0, y: 0 }, this.x = a.x, this.y = a.y }, extend: { clone: function () { return new a.Point(this) }, morph: function (t, e) { return this.destination = new a.Point(t, e), this } } }), a.extend(a.Element, { point: function (t, e) { return new a.Point(t, e).transform(this.screenCTM().inverse()) } }), a.extend(a.Element, { attr: function (t, e, s) { if (null == t) { for (t = {}, s = (e = this.node.attributes).length - 1; s >= 0; s--)t[e[s].nodeName] = a.regex.isNumber.test(e[s].nodeValue) ? parseFloat(e[s].nodeValue) : e[s].nodeValue; return t } if ("object" === i(t)) for (var r in t) this.attr(r, t[r]); else if (null === e) this.node.removeAttribute(t); else { if (null == e) return null == (e = this.node.getAttribute(t)) ? a.defaults.attrs[t] : a.regex.isNumber.test(e) ? parseFloat(e) : e; "stroke-width" == t ? this.attr("stroke", parseFloat(e) > 0 ? this._stroke : null) : "stroke" == t && (this._stroke = e), "fill" != t && "stroke" != t || (a.regex.isImage.test(e) && (e = this.doc().defs().image(e, 0, 0)), e instanceof a.Image && (e = this.doc().defs().pattern(0, 0, (function () { this.add(e) })))), "number" == typeof e ? e = new a.Number(e) : a.Color.isColor(e) ? e = new a.Color(e) : Array.isArray(e) && (e = new a.Array(e)), "leading" == t ? this.leading && this.leading(e) : "string" == typeof s ? this.node.setAttributeNS(s, t, e.toString()) : this.node.setAttribute(t, e.toString()), !this.rebuild || "font-size" != t && "x" != t || this.rebuild(t, e) } return this } }), a.extend(a.Element, { transform: function (t, e) { var s; return "object" !== i(t) ? (s = new a.Matrix(this).extract(), "string" == typeof t ? s[t] : s) : (s = new a.Matrix(this), e = !!e || !!t.relative, null != t.a && (s = e ? s.multiply(new a.Matrix(t)) : new a.Matrix(t)), this.attr("transform", s)) } }), a.extend(a.Element, { untransform: function () { return this.attr("transform", null) }, matrixify: function () { return (this.attr("transform") || "").split(a.regex.transforms).slice(0, -1).map((function (t) { var e = t.trim().split("("); return [e[0], e[1].split(a.regex.delimiter).map((function (t) { return parseFloat(t) }))] })).reduce((function (t, e) { return "matrix" == e[0] ? t.multiply(f(e[1])) : t[e[0]].apply(t, e[1]) }), new a.Matrix) }, toParent: function (t) { if (this == t) return this; var e = this.screenCTM(), i = t.screenCTM().inverse(); return this.addTo(t).untransform().transform(i.multiply(e)), this }, toDoc: function () { return this.toParent(this.doc()) } }), a.Transformation = a.invent({ create: function (t, e) { if (arguments.length > 1 && "boolean" != typeof e) return this.constructor.call(this, [].slice.call(arguments)); if (Array.isArray(t)) for (var a = 0, s = this.arguments.length; a < s; ++a)this[this.arguments[a]] = t[a]; else if (t && "object" === i(t)) for (a = 0, s = this.arguments.length; a < s; ++a)this[this.arguments[a]] = t[this.arguments[a]]; this.inversed = !1, !0 === e && (this.inversed = !0) } }), a.Translate = a.invent({ parent: a.Matrix, inherit: a.Transformation, create: function (t, e) { this.constructor.apply(this, [].slice.call(arguments)) }, extend: { arguments: ["transformedX", "transformedY"], method: "translate" } }), a.extend(a.Element, { style: function (t, e) { if (0 == arguments.length) return this.node.style.cssText || ""; if (arguments.length < 2) if ("object" === i(t)) for (var s in t) this.style(s, t[s]); else { if (!a.regex.isCss.test(t)) return this.node.style[c(t)]; for (t = t.split(/\s*;\s*/).filter((function (t) { return !!t })).map((function (t) { return t.split(/\s*:\s*/) })); e = t.pop();)this.style(e[0], e[1]) } else this.node.style[c(t)] = null === e || a.regex.isBlank.test(e) ? "" : e; return this } }), a.Parent = a.invent({ create: function (t) { this.constructor.call(this, t) }, inherit: a.Element, extend: { children: function () { return a.utils.map(a.utils.filterSVGElements(this.node.childNodes), (function (t) { return a.adopt(t) })) }, add: function (t, e) { return null == e ? this.node.appendChild(t.node) : t.node != this.node.childNodes[e] && this.node.insertBefore(t.node, this.node.childNodes[e]), this }, put: function (t, e) { return this.add(t, e), t }, has: function (t) { return this.index(t) >= 0 }, index: function (t) { return [].slice.call(this.node.childNodes).indexOf(t.node) }, get: function (t) { return a.adopt(this.node.childNodes[t]) }, first: function () { return this.get(0) }, last: function () { return this.get(this.node.childNodes.length - 1) }, each: function (t, e) { for (var i = this.children(), s = 0, r = i.length; s < r; s++)i[s] instanceof a.Element && t.apply(i[s], [s, i]), e && i[s] instanceof a.Container && i[s].each(t, e); return this }, removeElement: function (t) { return this.node.removeChild(t.node), this }, clear: function () { for (; this.node.hasChildNodes();)this.node.removeChild(this.node.lastChild); return delete this._defs, this }, defs: function () { return this.doc().defs() } } }), a.extend(a.Parent, { ungroup: function (t, e) { return 0 === e || this instanceof a.Defs || this.node == a.parser.draw || (t = t || (this instanceof a.Doc ? this : this.parent(a.Parent)), e = e || 1 / 0, this.each((function () { return this instanceof a.Defs ? this : this instanceof a.Parent ? this.ungroup(t, e - 1) : this.toParent(t) })), this.node.firstChild || this.remove()), this }, flatten: function (t, e) { return this.ungroup(t, e) } }), a.Container = a.invent({ create: function (t) { this.constructor.call(this, t) }, inherit: a.Parent }), a.ViewBox = a.invent({ parent: a.Container, construct: {} }), ["click", "dblclick", "mousedown", "mouseup", "mouseover", "mouseout", "mousemove", "touchstart", "touchmove", "touchleave", "touchend", "touchcancel"].forEach((function (t) { a.Element.prototype[t] = function (e) { return a.on(this.node, t, e), this } })), a.listeners = [], a.handlerMap = [], a.listenerId = 0, a.on = function (t, e, i, s, r) { var o = i.bind(s || t.instance || t), n = (a.handlerMap.indexOf(t) + 1 || a.handlerMap.push(t)) - 1, l = e.split(".")[0], h = e.split(".")[1] || "*"; a.listeners[n] = a.listeners[n] || {}, a.listeners[n][l] = a.listeners[n][l] || {}, a.listeners[n][l][h] = a.listeners[n][l][h] || {}, i._svgjsListenerId || (i._svgjsListenerId = ++a.listenerId), a.listeners[n][l][h][i._svgjsListenerId] = o, t.addEventListener(l, o, r || { passive: !1 }) }, a.off = function (t, e, i) { var s = a.handlerMap.indexOf(t), r = e && e.split(".")[0], o = e && e.split(".")[1], n = ""; if (-1 != s) if (i) { if ("function" == typeof i && (i = i._svgjsListenerId), !i) return; a.listeners[s][r] && a.listeners[s][r][o || "*"] && (t.removeEventListener(r, a.listeners[s][r][o || "*"][i], !1), delete a.listeners[s][r][o || "*"][i]) } else if (o && r) { if (a.listeners[s][r] && a.listeners[s][r][o]) { for (var l in a.listeners[s][r][o]) a.off(t, [r, o].join("."), l); delete a.listeners[s][r][o] } } else if (o) for (var h in a.listeners[s]) for (var n in a.listeners[s][h]) o === n && a.off(t, [h, o].join(".")); else if (r) { if (a.listeners[s][r]) { for (var n in a.listeners[s][r]) a.off(t, [r, n].join(".")); delete a.listeners[s][r] } } else { for (var h in a.listeners[s]) a.off(t, h); delete a.listeners[s], delete a.handlerMap[s] } }, a.extend(a.Element, { on: function (t, e, i, s) { return a.on(this.node, t, e, i, s), this }, off: function (t, e) { return a.off(this.node, t, e), this }, fire: function (e, i) { return e instanceof t.Event ? this.node.dispatchEvent(e) : this.node.dispatchEvent(e = new a.CustomEvent(e, { detail: i, cancelable: !0 })), this._event = e, this }, event: function () { return this._event } }), a.Defs = a.invent({ create: "defs", inherit: a.Container }), a.G = a.invent({ create: "g", inherit: a.Container, extend: { x: function (t) { return null == t ? this.transform("x") : this.transform({ x: t - this.x() }, !0) } }, construct: { group: function () { return this.put(new a.G) } } }), a.Doc = a.invent({ create: function (t) { t && ("svg" == (t = "string" == typeof t ? e.getElementById(t) : t).nodeName ? this.constructor.call(this, t) : (this.constructor.call(this, a.create("svg")), t.appendChild(this.node), this.size("100%", "100%")), this.namespace().defs()) }, inherit: a.Container, extend: { namespace: function () { return this.attr({ xmlns: a.ns, version: "1.1" }).attr("xmlns:xlink", a.xlink, a.xmlns).attr("xmlns:svgjs", a.svgjs, a.xmlns) }, defs: function () { var t; return this._defs || ((t = this.node.getElementsByTagName("defs")[0]) ? this._defs = a.adopt(t) : this._defs = new a.Defs, this.node.appendChild(this._defs.node)), this._defs }, parent: function () { return this.node.parentNode && "#document" != this.node.parentNode.nodeName ? this.node.parentNode : null }, remove: function () { return this.parent() && this.parent().removeChild(this.node), this }, clear: function () { for (; this.node.hasChildNodes();)this.node.removeChild(this.node.lastChild); return delete this._defs, a.parser.draw && !a.parser.draw.parentNode && this.node.appendChild(a.parser.draw), this }, clone: function (t) { this.writeDataToDom(); var e = this.node, i = x(e.cloneNode(!0)); return t ? (t.node || t).appendChild(i.node) : e.parentNode.insertBefore(i.node, e.nextSibling), i } } }), a.extend(a.Element, {}), a.Gradient = a.invent({ create: function (t) { this.constructor.call(this, a.create(t + "Gradient")), this.type = t }, inherit: a.Container, extend: { at: function (t, e, i) { return this.put(new a.Stop).update(t, e, i) }, update: function (t) { return this.clear(), "function" == typeof t && t.call(this, this), this }, fill: function () { return "url(#" + this.id() + ")" }, toString: function () { return this.fill() }, attr: function (t, e, i) { return "transform" == t && (t = "gradientTransform"), a.Container.prototype.attr.call(this, t, e, i) } }, construct: { gradient: function (t, e) { return this.defs().gradient(t, e) } } }), a.extend(a.Gradient, a.FX, { from: function (t, e) { return "radial" == (this._target || this).type ? this.attr({ fx: new a.Number(t), fy: new a.Number(e) }) : this.attr({ x1: new a.Number(t), y1: new a.Number(e) }) }, to: function (t, e) { return "radial" == (this._target || this).type ? this.attr({ cx: new a.Number(t), cy: new a.Number(e) }) : this.attr({ x2: new a.Number(t), y2: new a.Number(e) }) } }), a.extend(a.Defs, { gradient: function (t, e) { return this.put(new a.Gradient(t)).update(e) } }), a.Stop = a.invent({ create: "stop", inherit: a.Element, extend: { update: function (t) { return ("number" == typeof t || t instanceof a.Number) && (t = { offset: arguments[0], color: arguments[1], opacity: arguments[2] }), null != t.opacity && this.attr("stop-opacity", t.opacity), null != t.color && this.attr("stop-color", t.color), null != t.offset && this.attr("offset", new a.Number(t.offset)), this } } }), a.Pattern = a.invent({ create: "pattern", inherit: a.Container, extend: { fill: function () { return "url(#" + this.id() + ")" }, update: function (t) { return this.clear(), "function" == typeof t && t.call(this, this), this }, toString: function () { return this.fill() }, attr: function (t, e, i) { return "transform" == t && (t = "patternTransform"), a.Container.prototype.attr.call(this, t, e, i) } }, construct: { pattern: function (t, e, i) { return this.defs().pattern(t, e, i) } } }), a.extend(a.Defs, { pattern: function (t, e, i) { return this.put(new a.Pattern).update(i).attr({ x: 0, y: 0, width: t, height: e, patternUnits: "userSpaceOnUse" }) } }), a.Shape = a.invent({ create: function (t) { this.constructor.call(this, t) }, inherit: a.Element }), a.Symbol = a.invent({ create: "symbol", inherit: a.Container, construct: { symbol: function () { return this.put(new a.Symbol) } } }), a.Use = a.invent({ create: "use", inherit: a.Shape, extend: { element: function (t, e) { return this.attr("href", (e || "") + "#" + t, a.xlink) } }, construct: { use: function (t, e) { return this.put(new a.Use).element(t, e) } } }), a.Rect = a.invent({ create: "rect", inherit: a.Shape, construct: { rect: function (t, e) { return this.put(new a.Rect).size(t, e) } } }), a.Circle = a.invent({ create: "circle", inherit: a.Shape, construct: { circle: function (t) { return this.put(new a.Circle).rx(new a.Number(t).divide(2)).move(0, 0) } } }), a.extend(a.Circle, a.FX, { rx: function (t) { return this.attr("r", t) }, ry: function (t) { return this.rx(t) } }), a.Ellipse = a.invent({ create: "ellipse", inherit: a.Shape, construct: { ellipse: function (t, e) { return this.put(new a.Ellipse).size(t, e).move(0, 0) } } }), a.extend(a.Ellipse, a.Rect, a.FX, { rx: function (t) { return this.attr("rx", t) }, ry: function (t) { return this.attr("ry", t) } }), a.extend(a.Circle, a.Ellipse, { x: function (t) { return null == t ? this.cx() - this.rx() : this.cx(t + this.rx()) }, y: function (t) { return null == t ? this.cy() - this.ry() : this.cy(t + this.ry()) }, cx: function (t) { return null == t ? this.attr("cx") : this.attr("cx", t) }, cy: function (t) { return null == t ? this.attr("cy") : this.attr("cy", t) }, width: function (t) { return null == t ? 2 * this.rx() : this.rx(new a.Number(t).divide(2)) }, height: function (t) { return null == t ? 2 * this.ry() : this.ry(new a.Number(t).divide(2)) }, size: function (t, e) { var i = u(this, t, e); return this.rx(new a.Number(i.width).divide(2)).ry(new a.Number(i.height).divide(2)) } }), a.Line = a.invent({ create: "line", inherit: a.Shape, extend: { array: function () { return new a.PointArray([[this.attr("x1"), this.attr("y1")], [this.attr("x2"), this.attr("y2")]]) }, plot: function (t, e, i, s) { return null == t ? this.array() : (t = void 0 !== e ? { x1: t, y1: e, x2: i, y2: s } : new a.PointArray(t).toLine(), this.attr(t)) }, move: function (t, e) { return this.attr(this.array().move(t, e).toLine()) }, size: function (t, e) { var i = u(this, t, e); return this.attr(this.array().size(i.width, i.height).toLine()) } }, construct: { line: function (t, e, i, s) { return a.Line.prototype.plot.apply(this.put(new a.Line), null != t ? [t, e, i, s] : [0, 0, 0, 0]) } } }), a.Polyline = a.invent({ create: "polyline", inherit: a.Shape, construct: { polyline: function (t) { return this.put(new a.Polyline).plot(t || new a.PointArray) } } }), a.Polygon = a.invent({ create: "polygon", inherit: a.Shape, construct: { polygon: function (t) { return this.put(new a.Polygon).plot(t || new a.PointArray) } } }), a.extend(a.Polyline, a.Polygon, { array: function () { return this._array || (this._array = new a.PointArray(this.attr("points"))) }, plot: function (t) { return null == t ? this.array() : this.clear().attr("points", "string" == typeof t ? t : this._array = new a.PointArray(t)) }, clear: function () { return delete this._array, this }, move: function (t, e) { return this.attr("points", this.array().move(t, e)) }, size: function (t, e) { var i = u(this, t, e); return this.attr("points", this.array().size(i.width, i.height)) } }), a.extend(a.Line, a.Polyline, a.Polygon, { morphArray: a.PointArray, x: function (t) { return null == t ? this.bbox().x : this.move(t, this.bbox().y) }, y: function (t) { return null == t ? this.bbox().y : this.move(this.bbox().x, t) }, width: function (t) { var e = this.bbox(); return null == t ? e.width : this.size(t, e.height) }, height: function (t) { var e = this.bbox(); return null == t ? e.height : this.size(e.width, t) } }), a.Path = a.invent({ create: "path", inherit: a.Shape, extend: { morphArray: a.PathArray, array: function () { return this._array || (this._array = new a.PathArray(this.attr("d"))) }, plot: function (t) { return null == t ? this.array() : this.clear().attr("d", "string" == typeof t ? t : this._array = new a.PathArray(t)) }, clear: function () { return delete this._array, this } }, construct: { path: function (t) { return this.put(new a.Path).plot(t || new a.PathArray) } } }), a.Image = a.invent({ create: "image", inherit: a.Shape, extend: { load: function (e) { if (!e) return this; var i = this, s = new t.Image; return a.on(s, "load", (function () { a.off(s); var t = i.parent(a.Pattern); null !== t && (0 == i.width() && 0 == i.height() && i.size(s.width, s.height), t && 0 == t.width() && 0 == t.height() && t.size(i.width(), i.height()), "function" == typeof i._loaded && i._loaded.call(i, { width: s.width, height: s.height, ratio: s.width / s.height, url: e })) })), a.on(s, "error", (function (t) { a.off(s), "function" == typeof i._error && i._error.call(i, t) })), this.attr("href", s.src = this.src = e, a.xlink) }, loaded: function (t) { return this._loaded = t, this }, error: function (t) { return this._error = t, this } }, construct: { image: function (t, e, i) { return this.put(new a.Image).load(t).size(e || 0, i || e || 0) } } }), a.Text = a.invent({ create: function () { this.constructor.call(this, a.create("text")), this.dom.leading = new a.Number(1.3), this._rebuild = !0, this._build = !1, this.attr("font-family", a.defaults.attrs["font-family"]) }, inherit: a.Shape, extend: { x: function (t) { return null == t ? this.attr("x") : this.attr("x", t) }, text: function (t) { if (void 0 === t) { t = ""; for (var e = this.node.childNodes, i = 0, s = e.length; i < s; ++i)0 != i && 3 != e[i].nodeType && 1 == a.adopt(e[i]).dom.newLined && (t += "\n"), t += e[i].textContent; return t } if (this.clear().build(!0), "function" == typeof t) t.call(this, this); else { i = 0; for (var r = (t = t.split("\n")).length; i < r; i++)this.tspan(t[i]).newLine() } return this.build(!1).rebuild() }, size: function (t) { return this.attr("font-size", t).rebuild() }, leading: function (t) { return null == t ? this.dom.leading : (this.dom.leading = new a.Number(t), this.rebuild()) }, lines: function () { var t = (this.textPath && this.textPath() || this).node, e = a.utils.map(a.utils.filterSVGElements(t.childNodes), (function (t) { return a.adopt(t) })); return new a.Set(e) }, rebuild: function (t) { if ("boolean" == typeof t && (this._rebuild = t), this._rebuild) { var e = this, i = 0, s = this.dom.leading * new a.Number(this.attr("font-size")); this.lines().each((function () { this.dom.newLined && (e.textPath() || this.attr("x", e.attr("x")), "\n" == this.text() ? i += s : (this.attr("dy", s + i), i = 0)) })), this.fire("rebuild") } return this }, build: function (t) { return this._build = !!t, this }, setData: function (t) { return this.dom = t, this.dom.leading = new a.Number(t.leading || 1.3), this } }, construct: { text: function (t) { return this.put(new a.Text).text(t) }, plain: function (t) { return this.put(new a.Text).plain(t) } } }), a.Tspan = a.invent({ create: "tspan", inherit: a.Shape, extend: { text: function (t) { return null == t ? this.node.textContent + (this.dom.newLined ? "\n" : "") : ("function" == typeof t ? t.call(this, this) : this.plain(t), this) }, dx: function (t) { return this.attr("dx", t) }, dy: function (t) { return this.attr("dy", t) }, newLine: function () { var t = this.parent(a.Text); return this.dom.newLined = !0, this.dy(t.dom.leading * t.attr("font-size")).attr("x", t.x()) } } }), a.extend(a.Text, a.Tspan, { plain: function (t) { return !1 === this._build && this.clear(), this.node.appendChild(e.createTextNode(t)), this }, tspan: function (t) { var e = (this.textPath && this.textPath() || this).node, i = new a.Tspan; return !1 === this._build && this.clear(), e.appendChild(i.node), i.text(t) }, clear: function () { for (var t = (this.textPath && this.textPath() || this).node; t.hasChildNodes();)t.removeChild(t.lastChild); return this }, length: function () { return this.node.getComputedTextLength() } }), a.TextPath = a.invent({ create: "textPath", inherit: a.Parent, parent: a.Text, construct: { morphArray: a.PathArray, array: function () { var t = this.track(); return t ? t.array() : null }, plot: function (t) { var e = this.track(), i = null; return e && (i = e.plot(t)), null == t ? i : this }, track: function () { var t = this.textPath(); if (t) return t.reference("href") }, textPath: function () { if (this.node.firstChild && "textPath" == this.node.firstChild.nodeName) return a.adopt(this.node.firstChild) } } }), a.Nested = a.invent({ create: function () { this.constructor.call(this, a.create("svg")), this.style("overflow", "visible") }, inherit: a.Container, construct: { nested: function () { return this.put(new a.Nested) } } }); var l = { stroke: ["color", "width", "opacity", "linecap", "linejoin", "miterlimit", "dasharray", "dashoffset"], fill: ["color", "opacity", "rule"], prefix: function (t, e) { return "color" == e ? t : t + "-" + e } }; function h(t, e, i, s) { return i + s.replace(a.regex.dots, " .") } function c(t) { return t.toLowerCase().replace(/-(.)/g, (function (t, e) { return e.toUpperCase() })) } function d(t) { return t.charAt(0).toUpperCase() + t.slice(1) } function g(t) { var e = t.toString(16); return 1 == e.length ? "0" + e : e } function u(t, e, i) { if (null == e || null == i) { var a = t.bbox(); null == e ? e = a.width / a.height * i : null == i && (i = a.height / a.width * e) } return { width: e, height: i } } function p(t, e, i) { return { x: e * t.a + i * t.c + 0, y: e * t.b + i * t.d + 0 } } function f(t) { return { a: t[0], b: t[1], c: t[2], d: t[3], e: t[4], f: t[5] } } function x(e) { for (var i = e.childNodes.length - 1; i >= 0; i--)e.childNodes[i] instanceof t.SVGElement && x(e.childNodes[i]); return a.adopt(e).id(a.eid(e.nodeName)) } function b(t) { return Math.abs(t) > 1e-37 ? t : 0 } ["fill", "stroke"].forEach((function (t) { var e = {}; e[t] = function (e) { if (void 0 === e) return this; if ("string" == typeof e || a.Color.isRgb(e) || e && "function" == typeof e.fill) this.attr(t, e); else for (var i = l[t].length - 1; i >= 0; i--)null != e[l[t][i]] && this.attr(l.prefix(t, l[t][i]), e[l[t][i]]); return this }, a.extend(a.Element, a.FX, e) })), a.extend(a.Element, a.FX, { translate: function (t, e) { return this.transform({ x: t, y: e }) }, matrix: function (t) { return this.attr("transform", new a.Matrix(6 == arguments.length ? [].slice.call(arguments) : t)) }, opacity: function (t) { return this.attr("opacity", t) }, dx: function (t) { return this.x(new a.Number(t).plus(this instanceof a.FX ? 0 : this.x()), !0) }, dy: function (t) { return this.y(new a.Number(t).plus(this instanceof a.FX ? 0 : this.y()), !0) } }), a.extend(a.Path, { length: function () { return this.node.getTotalLength() }, pointAt: function (t) { return this.node.getPointAtLength(t) } }), a.Set = a.invent({ create: function (t) { Array.isArray(t) ? this.members = t : this.clear() }, extend: { add: function () { for (var t = [].slice.call(arguments), e = 0, i = t.length; e < i; e++)this.members.push(t[e]); return this }, remove: function (t) { var e = this.index(t); return e > -1 && this.members.splice(e, 1), this }, each: function (t) { for (var e = 0, i = this.members.length; e < i; e++)t.apply(this.members[e], [e, this.members]); return this }, clear: function () { return this.members = [], this }, length: function () { return this.members.length }, has: function (t) { return this.index(t) >= 0 }, index: function (t) { return this.members.indexOf(t) }, get: function (t) { return this.members[t] }, first: function () { return this.get(0) }, last: function () { return this.get(this.members.length - 1) }, valueOf: function () { return this.members } }, construct: { set: function (t) { return new a.Set(t) } } }), a.FX.Set = a.invent({ create: function (t) { this.set = t } }), a.Set.inherit = function () { var t = []; for (var e in a.Shape.prototype) "function" == typeof a.Shape.prototype[e] && "function" != typeof a.Set.prototype[e] && t.push(e); for (var e in t.forEach((function (t) { a.Set.prototype[t] = function () { for (var e = 0, i = this.members.length; e < i; e++)this.members[e] && "function" == typeof this.members[e][t] && this.members[e][t].apply(this.members[e], arguments); return "animate" == t ? this.fx || (this.fx = new a.FX.Set(this)) : this } })), t = [], a.FX.prototype) "function" == typeof a.FX.prototype[e] && "function" != typeof a.FX.Set.prototype[e] && t.push(e); t.forEach((function (t) { a.FX.Set.prototype[t] = function () { for (var e = 0, i = this.set.members.length; e < i; e++)this.set.members[e].fx[t].apply(this.set.members[e].fx, arguments); return this } })) }, a.extend(a.Element, {}), a.extend(a.Element, { remember: function (t, e) { if ("object" === i(arguments[0])) for (var a in t) this.remember(a, t[a]); else { if (1 == arguments.length) return this.memory()[t]; this.memory()[t] = e } return this }, forget: function () { if (0 == arguments.length) this._memory = {}; else for (var t = arguments.length - 1; t >= 0; t--)delete this.memory()[arguments[t]]; return this }, memory: function () { return this._memory || (this._memory = {}) } }), a.get = function (t) { var i = e.getElementById(function (t) { var e = (t || "").toString().match(a.regex.reference); if (e) return e[1] }(t) || t); return a.adopt(i) }, a.select = function (t, i) { return new a.Set(a.utils.map((i || e).querySelectorAll(t), (function (t) { return a.adopt(t) }))) }, a.extend(a.Parent, { select: function (t) { return a.select(t, this.node) } }); var v = "abcdef".split(""); if ("function" != typeof t.CustomEvent) { var m = function (t, i) { i = i || { bubbles: !1, cancelable: !1, detail: void 0 }; var a = e.createEvent("CustomEvent"); return a.initCustomEvent(t, i.bubbles, i.cancelable, i.detail), a }; m.prototype = t.Event.prototype, a.CustomEvent = m } else a.CustomEvent = t.CustomEvent; return a }, "function" == typeof define && define.amd ? define((function () { return Ht(Rt, Rt.document) })) : "object" === ("undefined" == typeof exports ? "undefined" : i(exports)) && "undefined" != typeof module ? module.exports = Rt.document ? Ht(Rt, Rt.document) : function (t) { return Ht(t, t.document) } : Rt.SVG = Ht(Rt, Rt.document), + /*! svg.filter.js - v2.0.2 - 2016-02-24 + * https://github.com/wout/svg.filter.js + * Copyright (c) 2016 Wout Fierens; Licensed MIT */ + function () { SVG.Filter = SVG.invent({ create: "filter", inherit: SVG.Parent, extend: { source: "SourceGraphic", sourceAlpha: "SourceAlpha", background: "BackgroundImage", backgroundAlpha: "BackgroundAlpha", fill: "FillPaint", stroke: "StrokePaint", autoSetIn: !0, put: function (t, e) { return this.add(t, e), !t.attr("in") && this.autoSetIn && t.attr("in", this.source), t.attr("result") || t.attr("result", t), t }, blend: function (t, e, i) { return this.put(new SVG.BlendEffect(t, e, i)) }, colorMatrix: function (t, e) { return this.put(new SVG.ColorMatrixEffect(t, e)) }, convolveMatrix: function (t) { return this.put(new SVG.ConvolveMatrixEffect(t)) }, componentTransfer: function (t) { return this.put(new SVG.ComponentTransferEffect(t)) }, composite: function (t, e, i) { return this.put(new SVG.CompositeEffect(t, e, i)) }, flood: function (t, e) { return this.put(new SVG.FloodEffect(t, e)) }, offset: function (t, e) { return this.put(new SVG.OffsetEffect(t, e)) }, image: function (t) { return this.put(new SVG.ImageEffect(t)) }, merge: function () { var t = [void 0]; for (var e in arguments) t.push(arguments[e]); return this.put(new (SVG.MergeEffect.bind.apply(SVG.MergeEffect, t))) }, gaussianBlur: function (t, e) { return this.put(new SVG.GaussianBlurEffect(t, e)) }, morphology: function (t, e) { return this.put(new SVG.MorphologyEffect(t, e)) }, diffuseLighting: function (t, e, i) { return this.put(new SVG.DiffuseLightingEffect(t, e, i)) }, displacementMap: function (t, e, i, a, s) { return this.put(new SVG.DisplacementMapEffect(t, e, i, a, s)) }, specularLighting: function (t, e, i, a) { return this.put(new SVG.SpecularLightingEffect(t, e, i, a)) }, tile: function () { return this.put(new SVG.TileEffect) }, turbulence: function (t, e, i, a, s) { return this.put(new SVG.TurbulenceEffect(t, e, i, a, s)) }, toString: function () { return "url(#" + this.attr("id") + ")" } } }), SVG.extend(SVG.Defs, { filter: function (t) { var e = this.put(new SVG.Filter); return "function" == typeof t && t.call(e, e), e } }), SVG.extend(SVG.Container, { filter: function (t) { return this.defs().filter(t) } }), SVG.extend(SVG.Element, SVG.G, SVG.Nested, { filter: function (t) { return this.filterer = t instanceof SVG.Element ? t : this.doc().filter(t), this.doc() && this.filterer.doc() !== this.doc() && this.doc().defs().add(this.filterer), this.attr("filter", this.filterer), this.filterer }, unfilter: function (t) { return this.filterer && !0 === t && this.filterer.remove(), delete this.filterer, this.attr("filter", null) } }), SVG.Effect = SVG.invent({ create: function () { this.constructor.call(this) }, inherit: SVG.Element, extend: { in: function (t) { return null == t ? this.parent() && this.parent().select('[result="' + this.attr("in") + '"]').get(0) || this.attr("in") : this.attr("in", t) }, result: function (t) { return null == t ? this.attr("result") : this.attr("result", t) }, toString: function () { return this.result() } } }), SVG.ParentEffect = SVG.invent({ create: function () { this.constructor.call(this) }, inherit: SVG.Parent, extend: { in: function (t) { return null == t ? this.parent() && this.parent().select('[result="' + this.attr("in") + '"]').get(0) || this.attr("in") : this.attr("in", t) }, result: function (t) { return null == t ? this.attr("result") : this.attr("result", t) }, toString: function () { return this.result() } } }); var t = { blend: function (t, e) { return this.parent() && this.parent().blend(this, t, e) }, colorMatrix: function (t, e) { return this.parent() && this.parent().colorMatrix(t, e).in(this) }, convolveMatrix: function (t) { return this.parent() && this.parent().convolveMatrix(t).in(this) }, componentTransfer: function (t) { return this.parent() && this.parent().componentTransfer(t).in(this) }, composite: function (t, e) { return this.parent() && this.parent().composite(this, t, e) }, flood: function (t, e) { return this.parent() && this.parent().flood(t, e) }, offset: function (t, e) { return this.parent() && this.parent().offset(t, e).in(this) }, image: function (t) { return this.parent() && this.parent().image(t) }, merge: function () { return this.parent() && this.parent().merge.apply(this.parent(), [this].concat(arguments)) }, gaussianBlur: function (t, e) { return this.parent() && this.parent().gaussianBlur(t, e).in(this) }, morphology: function (t, e) { return this.parent() && this.parent().morphology(t, e).in(this) }, diffuseLighting: function (t, e, i) { return this.parent() && this.parent().diffuseLighting(t, e, i).in(this) }, displacementMap: function (t, e, i, a) { return this.parent() && this.parent().displacementMap(this, t, e, i, a) }, specularLighting: function (t, e, i, a) { return this.parent() && this.parent().specularLighting(t, e, i, a).in(this) }, tile: function () { return this.parent() && this.parent().tile().in(this) }, turbulence: function (t, e, i, a, s) { return this.parent() && this.parent().turbulence(t, e, i, a, s).in(this) } }; SVG.extend(SVG.Effect, t), SVG.extend(SVG.ParentEffect, t), SVG.ChildEffect = SVG.invent({ create: function () { this.constructor.call(this) }, inherit: SVG.Element, extend: { in: function (t) { this.attr("in", t) } } }); var e = { blend: function (t, e, i) { this.attr({ in: t, in2: e, mode: i || "normal" }) }, colorMatrix: function (t, e) { "matrix" == t && (e = s(e)), this.attr({ type: t, values: void 0 === e ? null : e }) }, convolveMatrix: function (t) { t = s(t), this.attr({ order: Math.sqrt(t.split(" ").length), kernelMatrix: t }) }, composite: function (t, e, i) { this.attr({ in: t, in2: e, operator: i }) }, flood: function (t, e) { this.attr("flood-color", t), null != e && this.attr("flood-opacity", e) }, offset: function (t, e) { this.attr({ dx: t, dy: e }) }, image: function (t) { this.attr("href", t, SVG.xlink) }, displacementMap: function (t, e, i, a, s) { this.attr({ in: t, in2: e, scale: i, xChannelSelector: a, yChannelSelector: s }) }, gaussianBlur: function (t, e) { null != t || null != e ? this.attr("stdDeviation", function (t) { if (!Array.isArray(t)) return t; for (var e = 0, i = t.length, a = []; e < i; e++)a.push(t[e]); return a.join(" ") }(Array.prototype.slice.call(arguments))) : this.attr("stdDeviation", "0 0") }, morphology: function (t, e) { this.attr({ operator: t, radius: e }) }, tile: function () { }, turbulence: function (t, e, i, a, s) { this.attr({ numOctaves: e, seed: i, stitchTiles: a, baseFrequency: t, type: s }) } }, i = { merge: function () { var t; if (arguments[0] instanceof SVG.Set) { var e = this; arguments[0].each((function (t) { this instanceof SVG.MergeNode ? e.put(this) : (this instanceof SVG.Effect || this instanceof SVG.ParentEffect) && e.put(new SVG.MergeNode(this)) })) } else { t = Array.isArray(arguments[0]) ? arguments[0] : arguments; for (var i = 0; i < t.length; i++)t[i] instanceof SVG.MergeNode ? this.put(t[i]) : this.put(new SVG.MergeNode(t[i])) } }, componentTransfer: function (t) { if (this.rgb = new SVG.Set, ["r", "g", "b", "a"].forEach(function (t) { this[t] = new (SVG["Func" + t.toUpperCase()])("identity"), this.rgb.add(this[t]), this.node.appendChild(this[t].node) }.bind(this)), t) for (var e in t.rgb && (["r", "g", "b"].forEach(function (e) { this[e].attr(t.rgb) }.bind(this)), delete t.rgb), t) this[e].attr(t[e]) }, diffuseLighting: function (t, e, i) { this.attr({ surfaceScale: t, diffuseConstant: e, kernelUnitLength: i }) }, specularLighting: function (t, e, i, a) { this.attr({ surfaceScale: t, diffuseConstant: e, specularExponent: i, kernelUnitLength: a }) } }, a = { distantLight: function (t, e) { this.attr({ azimuth: t, elevation: e }) }, pointLight: function (t, e, i) { this.attr({ x: t, y: e, z: i }) }, spotLight: function (t, e, i, a, s, r) { this.attr({ x: t, y: e, z: i, pointsAtX: a, pointsAtY: s, pointsAtZ: r }) }, mergeNode: function (t) { this.attr("in", t) } }; function s(t) { return Array.isArray(t) && (t = new SVG.Array(t)), t.toString().replace(/^\s+/, "").replace(/\s+$/, "").replace(/\s+/g, " ") } function r() { var t = function () { }; for (var e in "function" == typeof arguments[arguments.length - 1] && (t = arguments[arguments.length - 1], Array.prototype.splice.call(arguments, arguments.length - 1, 1)), arguments) for (var i in arguments[e]) t(arguments[e][i], i, arguments[e]) } ["r", "g", "b", "a"].forEach((function (t) { a["Func" + t.toUpperCase()] = function (t) { switch (this.attr("type", t), t) { case "table": this.attr("tableValues", arguments[1]); break; case "linear": this.attr("slope", arguments[1]), this.attr("intercept", arguments[2]); break; case "gamma": this.attr("amplitude", arguments[1]), this.attr("exponent", arguments[2]), this.attr("offset", arguments[2]) } } })), r(e, (function (t, e) { var i = e.charAt(0).toUpperCase() + e.slice(1); SVG[i + "Effect"] = SVG.invent({ create: function () { this.constructor.call(this, SVG.create("fe" + i)), t.apply(this, arguments), this.result(this.attr("id") + "Out") }, inherit: SVG.Effect, extend: {} }) })), r(i, (function (t, e) { var i = e.charAt(0).toUpperCase() + e.slice(1); SVG[i + "Effect"] = SVG.invent({ create: function () { this.constructor.call(this, SVG.create("fe" + i)), t.apply(this, arguments), this.result(this.attr("id") + "Out") }, inherit: SVG.ParentEffect, extend: {} }) })), r(a, (function (t, e) { var i = e.charAt(0).toUpperCase() + e.slice(1); SVG[i] = SVG.invent({ create: function () { this.constructor.call(this, SVG.create("fe" + i)), t.apply(this, arguments) }, inherit: SVG.ChildEffect, extend: {} }) })), SVG.extend(SVG.MergeEffect, { in: function (t) { return t instanceof SVG.MergeNode ? this.add(t, 0) : this.add(new SVG.MergeNode(t), 0), this } }), SVG.extend(SVG.CompositeEffect, SVG.BlendEffect, SVG.DisplacementMapEffect, { in2: function (t) { return null == t ? this.parent() && this.parent().select('[result="' + this.attr("in2") + '"]').get(0) || this.attr("in2") : this.attr("in2", t) } }), SVG.filter = { sepiatone: [.343, .669, .119, 0, 0, .249, .626, .13, 0, 0, .172, .334, .111, 0, 0, 0, 0, 0, 1, 0] } }.call(void 0), function () { function t(t, s, r, o, n, l, h) { for (var c = t.slice(s, r || h), d = o.slice(n, l || h), g = 0, u = { pos: [0, 0], start: [0, 0] }, p = { pos: [0, 0], start: [0, 0] }; ;) { if (c[g] = e.call(u, c[g]), d[g] = e.call(p, d[g]), c[g][0] != d[g][0] || "M" == c[g][0] || "A" == c[g][0] && (c[g][4] != d[g][4] || c[g][5] != d[g][5]) ? (Array.prototype.splice.apply(c, [g, 1].concat(a.call(u, c[g]))), Array.prototype.splice.apply(d, [g, 1].concat(a.call(p, d[g])))) : (c[g] = i.call(u, c[g]), d[g] = i.call(p, d[g])), ++g == c.length && g == d.length) break; g == c.length && c.push(["C", u.pos[0], u.pos[1], u.pos[0], u.pos[1], u.pos[0], u.pos[1]]), g == d.length && d.push(["C", p.pos[0], p.pos[1], p.pos[0], p.pos[1], p.pos[0], p.pos[1]]) } return { start: c, dest: d } } function e(t) { switch (t[0]) { case "z": case "Z": t[0] = "L", t[1] = this.start[0], t[2] = this.start[1]; break; case "H": t[0] = "L", t[2] = this.pos[1]; break; case "V": t[0] = "L", t[2] = t[1], t[1] = this.pos[0]; break; case "T": t[0] = "Q", t[3] = t[1], t[4] = t[2], t[1] = this.reflection[1], t[2] = this.reflection[0]; break; case "S": t[0] = "C", t[6] = t[4], t[5] = t[3], t[4] = t[2], t[3] = t[1], t[2] = this.reflection[1], t[1] = this.reflection[0] }return t } function i(t) { var e = t.length; return this.pos = [t[e - 2], t[e - 1]], -1 != "SCQT".indexOf(t[0]) && (this.reflection = [2 * this.pos[0] - t[e - 4], 2 * this.pos[1] - t[e - 3]]), t } function a(t) { var e = [t]; switch (t[0]) { case "M": return this.pos = this.start = [t[1], t[2]], e; case "L": t[5] = t[3] = t[1], t[6] = t[4] = t[2], t[1] = this.pos[0], t[2] = this.pos[1]; break; case "Q": t[6] = t[4], t[5] = t[3], t[4] = 1 * t[4] / 3 + 2 * t[2] / 3, t[3] = 1 * t[3] / 3 + 2 * t[1] / 3, t[2] = 1 * this.pos[1] / 3 + 2 * t[2] / 3, t[1] = 1 * this.pos[0] / 3 + 2 * t[1] / 3; break; case "A": e = function (t, e) { var i, a, s, r, o, n, l, h, c, d, g, u, p, f, x, b, v, m, y, w, k, A, S, C, L, P, M = Math.abs(e[1]), I = Math.abs(e[2]), T = e[3] % 360, z = e[4], X = e[5], E = e[6], Y = e[7], F = new SVG.Point(t), R = new SVG.Point(E, Y), H = []; if (0 === M || 0 === I || F.x === R.x && F.y === R.y) return [["C", F.x, F.y, R.x, R.y, R.x, R.y]]; i = new SVG.Point((F.x - R.x) / 2, (F.y - R.y) / 2).transform((new SVG.Matrix).rotate(T)), (a = i.x * i.x / (M * M) + i.y * i.y / (I * I)) > 1 && (M *= a = Math.sqrt(a), I *= a); s = (new SVG.Matrix).rotate(T).scale(1 / M, 1 / I).rotate(-T), F = F.transform(s), R = R.transform(s), r = [R.x - F.x, R.y - F.y], n = r[0] * r[0] + r[1] * r[1], o = Math.sqrt(n), r[0] /= o, r[1] /= o, l = n < 4 ? Math.sqrt(1 - n / 4) : 0, z === X && (l *= -1); h = new SVG.Point((R.x + F.x) / 2 + l * -r[1], (R.y + F.y) / 2 + l * r[0]), c = new SVG.Point(F.x - h.x, F.y - h.y), d = new SVG.Point(R.x - h.x, R.y - h.y), g = Math.acos(c.x / Math.sqrt(c.x * c.x + c.y * c.y)), c.y < 0 && (g *= -1); u = Math.acos(d.x / Math.sqrt(d.x * d.x + d.y * d.y)), d.y < 0 && (u *= -1); X && g > u && (u += 2 * Math.PI); !X && g < u && (u -= 2 * Math.PI); for (f = Math.ceil(2 * Math.abs(g - u) / Math.PI), b = [], v = g, p = (u - g) / f, x = 4 * Math.tan(p / 4) / 3, k = 0; k <= f; k++)y = Math.cos(v), m = Math.sin(v), w = new SVG.Point(h.x + y, h.y + m), b[k] = [new SVG.Point(w.x + x * m, w.y - x * y), w, new SVG.Point(w.x - x * m, w.y + x * y)], v += p; for (b[0][0] = b[0][1].clone(), b[b.length - 1][2] = b[b.length - 1][1].clone(), s = (new SVG.Matrix).rotate(T).scale(M, I).rotate(-T), k = 0, A = b.length; k < A; k++)b[k][0] = b[k][0].transform(s), b[k][1] = b[k][1].transform(s), b[k][2] = b[k][2].transform(s); for (k = 1, A = b.length; k < A; k++)S = (w = b[k - 1][2]).x, C = w.y, L = (w = b[k][0]).x, P = w.y, E = (w = b[k][1]).x, Y = w.y, H.push(["C", S, C, L, P, E, Y]); return H }(this.pos, t), t = e[0] }return t[0] = "C", this.pos = [t[5], t[6]], this.reflection = [2 * t[5] - t[3], 2 * t[6] - t[4]], e } function s(t, e) { if (!1 === e) return !1; for (var i = e, a = t.length; i < a; ++i)if ("M" == t[i][0]) return i; return !1 } SVG.extend(SVG.PathArray, { morph: function (e) { for (var i = this.value, a = this.parse(e), r = 0, o = 0, n = !1, l = !1; !1 !== r || !1 !== o;) { var h; n = s(i, !1 !== r && r + 1), l = s(a, !1 !== o && o + 1), !1 === r && (r = 0 == (h = new SVG.PathArray(c.start).bbox()).height || 0 == h.width ? i.push(i[0]) - 1 : i.push(["M", h.x + h.width / 2, h.y + h.height / 2]) - 1), !1 === o && (o = 0 == (h = new SVG.PathArray(c.dest).bbox()).height || 0 == h.width ? a.push(a[0]) - 1 : a.push(["M", h.x + h.width / 2, h.y + h.height / 2]) - 1); var c = t(i, r, n, a, o, l); i = i.slice(0, r).concat(c.start, !1 === n ? [] : i.slice(n)), a = a.slice(0, o).concat(c.dest, !1 === l ? [] : a.slice(l)), r = !1 !== n && r + c.start.length, o = !1 !== l && o + c.dest.length } return this.value = i, this.destination = new SVG.PathArray, this.destination.value = a, this } }) }(), + /*! svg.draggable.js - v2.2.2 - 2019-01-08 + * https://github.com/svgdotjs/svg.draggable.js + * Copyright (c) 2019 Wout Fierens; Licensed MIT */ + function () { function t(t) { t.remember("_draggable", this), this.el = t } t.prototype.init = function (t, e) { var i = this; this.constraint = t, this.value = e, this.el.on("mousedown.drag", (function (t) { i.start(t) })), this.el.on("touchstart.drag", (function (t) { i.start(t) })) }, t.prototype.transformPoint = function (t, e) { var i = (t = t || window.event).changedTouches && t.changedTouches[0] || t; return this.p.x = i.clientX - (e || 0), this.p.y = i.clientY, this.p.matrixTransform(this.m) }, t.prototype.getBBox = function () { var t = this.el.bbox(); return this.el instanceof SVG.Nested && (t = this.el.rbox()), (this.el instanceof SVG.G || this.el instanceof SVG.Use || this.el instanceof SVG.Nested) && (t.x = this.el.x(), t.y = this.el.y()), t }, t.prototype.start = function (t) { if ("click" != t.type && "mousedown" != t.type && "mousemove" != t.type || 1 == (t.which || t.buttons)) { var e = this; if (this.el.fire("beforedrag", { event: t, handler: this }), !this.el.event().defaultPrevented) { t.preventDefault(), t.stopPropagation(), this.parent = this.parent || this.el.parent(SVG.Nested) || this.el.parent(SVG.Doc), this.p = this.parent.node.createSVGPoint(), this.m = this.el.node.getScreenCTM().inverse(); var i, a = this.getBBox(); if (this.el instanceof SVG.Text) switch (i = this.el.node.getComputedTextLength(), this.el.attr("text-anchor")) { case "middle": i /= 2; break; case "start": i = 0 }this.startPoints = { point: this.transformPoint(t, i), box: a, transform: this.el.transform() }, SVG.on(window, "mousemove.drag", (function (t) { e.drag(t) })), SVG.on(window, "touchmove.drag", (function (t) { e.drag(t) })), SVG.on(window, "mouseup.drag", (function (t) { e.end(t) })), SVG.on(window, "touchend.drag", (function (t) { e.end(t) })), this.el.fire("dragstart", { event: t, p: this.startPoints.point, m: this.m, handler: this }) } } }, t.prototype.drag = function (t) { var e = this.getBBox(), i = this.transformPoint(t), a = this.startPoints.box.x + i.x - this.startPoints.point.x, s = this.startPoints.box.y + i.y - this.startPoints.point.y, r = this.constraint, o = i.x - this.startPoints.point.x, n = i.y - this.startPoints.point.y; if (this.el.fire("dragmove", { event: t, p: i, m: this.m, handler: this }), this.el.event().defaultPrevented) return i; if ("function" == typeof r) { var l = r.call(this.el, a, s, this.m); "boolean" == typeof l && (l = { x: l, y: l }), !0 === l.x ? this.el.x(a) : !1 !== l.x && this.el.x(l.x), !0 === l.y ? this.el.y(s) : !1 !== l.y && this.el.y(l.y) } else "object" == typeof r && (null != r.minX && a < r.minX ? o = (a = r.minX) - this.startPoints.box.x : null != r.maxX && a > r.maxX - e.width && (o = (a = r.maxX - e.width) - this.startPoints.box.x), null != r.minY && s < r.minY ? n = (s = r.minY) - this.startPoints.box.y : null != r.maxY && s > r.maxY - e.height && (n = (s = r.maxY - e.height) - this.startPoints.box.y), null != r.snapToGrid && (a -= a % r.snapToGrid, s -= s % r.snapToGrid, o -= o % r.snapToGrid, n -= n % r.snapToGrid), this.el instanceof SVG.G ? this.el.matrix(this.startPoints.transform).transform({ x: o, y: n }, !0) : this.el.move(a, s)); return i }, t.prototype.end = function (t) { var e = this.drag(t); this.el.fire("dragend", { event: t, p: e, m: this.m, handler: this }), SVG.off(window, "mousemove.drag"), SVG.off(window, "touchmove.drag"), SVG.off(window, "mouseup.drag"), SVG.off(window, "touchend.drag") }, SVG.extend(SVG.Element, { draggable: function (e, i) { "function" != typeof e && "object" != typeof e || (i = e, e = !0); var a = this.remember("_draggable") || new t(this); return (e = void 0 === e || e) ? a.init(i || {}, e) : (this.off("mousedown.drag"), this.off("touchstart.drag")), this } }) }.call(void 0), function () { function t(t) { this.el = t, t.remember("_selectHandler", this), this.pointSelection = { isSelected: !1 }, this.rectSelection = { isSelected: !1 }, this.pointsList = { lt: [0, 0], rt: ["width", 0], rb: ["width", "height"], lb: [0, "height"], t: ["width", 0], r: ["width", "height"], b: ["width", "height"], l: [0, "height"] }, this.pointCoord = function (t, e, i) { var a = "string" != typeof t ? t : e[t]; return i ? a / 2 : a }, this.pointCoords = function (t, e) { var i = this.pointsList[t]; return { x: this.pointCoord(i[0], e, "t" === t || "b" === t), y: this.pointCoord(i[1], e, "r" === t || "l" === t) } } } t.prototype.init = function (t, e) { var i = this.el.bbox(); this.options = {}; var a = this.el.selectize.defaults.points; for (var s in this.el.selectize.defaults) this.options[s] = this.el.selectize.defaults[s], void 0 !== e[s] && (this.options[s] = e[s]); var r = ["points", "pointsExclude"]; for (var s in r) { var o = this.options[r[s]]; "string" == typeof o ? o = o.length > 0 ? o.split(/\s*,\s*/i) : [] : "boolean" == typeof o && "points" === r[s] && (o = o ? a : []), this.options[r[s]] = o } this.options.points = [a, this.options.points].reduce((function (t, e) { return t.filter((function (t) { return e.indexOf(t) > -1 })) })), this.options.points = [this.options.points, this.options.pointsExclude].reduce((function (t, e) { return t.filter((function (t) { return e.indexOf(t) < 0 })) })), this.parent = this.el.parent(), this.nested = this.nested || this.parent.group(), this.nested.matrix(new SVG.Matrix(this.el).translate(i.x, i.y)), this.options.deepSelect && -1 !== ["line", "polyline", "polygon"].indexOf(this.el.type) ? this.selectPoints(t) : this.selectRect(t), this.observe(), this.cleanup() }, t.prototype.selectPoints = function (t) { return this.pointSelection.isSelected = t, this.pointSelection.set || (this.pointSelection.set = this.parent.set(), this.drawPoints()), this }, t.prototype.getPointArray = function () { var t = this.el.bbox(); return this.el.array().valueOf().map((function (e) { return [e[0] - t.x, e[1] - t.y] })) }, t.prototype.drawPoints = function () { for (var t = this, e = this.getPointArray(), i = 0, a = e.length; i < a; ++i) { var s = function (e) { return function (i) { (i = i || window.event).preventDefault ? i.preventDefault() : i.returnValue = !1, i.stopPropagation(); var a = i.pageX || i.touches[0].pageX, s = i.pageY || i.touches[0].pageY; t.el.fire("point", { x: a, y: s, i: e, event: i }) } }(i), r = this.drawPoint(e[i][0], e[i][1]).addClass(this.options.classPoints).addClass(this.options.classPoints + "_point").on("touchstart", s).on("mousedown", s); this.pointSelection.set.add(r) } }, t.prototype.drawPoint = function (t, e) { var i = this.options.pointType; switch (i) { case "circle": return this.drawCircle(t, e); case "rect": return this.drawRect(t, e); default: if ("function" == typeof i) return i.call(this, t, e); throw new Error("Unknown " + i + " point type!") } }, t.prototype.drawCircle = function (t, e) { return this.nested.circle(this.options.pointSize).center(t, e) }, t.prototype.drawRect = function (t, e) { return this.nested.rect(this.options.pointSize, this.options.pointSize).center(t, e) }, t.prototype.updatePointSelection = function () { var t = this.getPointArray(); this.pointSelection.set.each((function (e) { this.cx() === t[e][0] && this.cy() === t[e][1] || this.center(t[e][0], t[e][1]) })) }, t.prototype.updateRectSelection = function () { var t = this, e = this.el.bbox(); if (this.rectSelection.set.get(0).attr({ width: e.width, height: e.height }), this.options.points.length && this.options.points.map((function (i, a) { var s = t.pointCoords(i, e); t.rectSelection.set.get(a + 1).center(s.x, s.y) })), this.options.rotationPoint) { var i = this.rectSelection.set.length(); this.rectSelection.set.get(i - 1).center(e.width / 2, 20) } }, t.prototype.selectRect = function (t) { var e = this, i = this.el.bbox(); function a(t) { return function (i) { (i = i || window.event).preventDefault ? i.preventDefault() : i.returnValue = !1, i.stopPropagation(); var a = i.pageX || i.touches[0].pageX, s = i.pageY || i.touches[0].pageY; e.el.fire(t, { x: a, y: s, event: i }) } } if (this.rectSelection.isSelected = t, this.rectSelection.set = this.rectSelection.set || this.parent.set(), this.rectSelection.set.get(0) || this.rectSelection.set.add(this.nested.rect(i.width, i.height).addClass(this.options.classRect)), this.options.points.length && this.rectSelection.set.length() < 2) { this.options.points.map((function (t, s) { var r = e.pointCoords(t, i), o = e.drawPoint(r.x, r.y).attr("class", e.options.classPoints + "_" + t).on("mousedown", a(t)).on("touchstart", a(t)); e.rectSelection.set.add(o) })), this.rectSelection.set.each((function () { this.addClass(e.options.classPoints) })) } if (this.options.rotationPoint && (this.options.points && !this.rectSelection.set.get(9) || !this.options.points && !this.rectSelection.set.get(1))) { var s = function (t) { (t = t || window.event).preventDefault ? t.preventDefault() : t.returnValue = !1, t.stopPropagation(); var i = t.pageX || t.touches[0].pageX, a = t.pageY || t.touches[0].pageY; e.el.fire("rot", { x: i, y: a, event: t }) }, r = this.drawPoint(i.width / 2, 20).attr("class", this.options.classPoints + "_rot").on("touchstart", s).on("mousedown", s); this.rectSelection.set.add(r) } }, t.prototype.handler = function () { var t = this.el.bbox(); this.nested.matrix(new SVG.Matrix(this.el).translate(t.x, t.y)), this.rectSelection.isSelected && this.updateRectSelection(), this.pointSelection.isSelected && this.updatePointSelection() }, t.prototype.observe = function () { var t = this; if (MutationObserver) if (this.rectSelection.isSelected || this.pointSelection.isSelected) this.observerInst = this.observerInst || new MutationObserver((function () { t.handler() })), this.observerInst.observe(this.el.node, { attributes: !0 }); else try { this.observerInst.disconnect(), delete this.observerInst } catch (t) { } else this.el.off("DOMAttrModified.select"), (this.rectSelection.isSelected || this.pointSelection.isSelected) && this.el.on("DOMAttrModified.select", (function () { t.handler() })) }, t.prototype.cleanup = function () { !this.rectSelection.isSelected && this.rectSelection.set && (this.rectSelection.set.each((function () { this.remove() })), this.rectSelection.set.clear(), delete this.rectSelection.set), !this.pointSelection.isSelected && this.pointSelection.set && (this.pointSelection.set.each((function () { this.remove() })), this.pointSelection.set.clear(), delete this.pointSelection.set), this.pointSelection.isSelected || this.rectSelection.isSelected || (this.nested.remove(), delete this.nested) }, SVG.extend(SVG.Element, { selectize: function (e, i) { return "object" == typeof e && (i = e, e = !0), (this.remember("_selectHandler") || new t(this)).init(void 0 === e || e, i || {}), this } }), SVG.Element.prototype.selectize.defaults = { points: ["lt", "rt", "rb", "lb", "t", "r", "b", "l"], pointsExclude: [], classRect: "svg_select_boundingRect", classPoints: "svg_select_points", pointSize: 7, rotationPoint: !0, deepSelect: !1, pointType: "circle" } }(), function () { (function () { function t(t) { t.remember("_resizeHandler", this), this.el = t, this.parameters = {}, this.lastUpdateCall = null, this.p = t.doc().node.createSVGPoint() } t.prototype.transformPoint = function (t, e, i) { return this.p.x = t - (this.offset.x - window.pageXOffset), this.p.y = e - (this.offset.y - window.pageYOffset), this.p.matrixTransform(i || this.m) }, t.prototype._extractPosition = function (t) { return { x: null != t.clientX ? t.clientX : t.touches[0].clientX, y: null != t.clientY ? t.clientY : t.touches[0].clientY } }, t.prototype.init = function (t) { var e = this; if (this.stop(), "stop" !== t) { for (var i in this.options = {}, this.el.resize.defaults) this.options[i] = this.el.resize.defaults[i], void 0 !== t[i] && (this.options[i] = t[i]); this.el.on("lt.resize", (function (t) { e.resize(t || window.event) })), this.el.on("rt.resize", (function (t) { e.resize(t || window.event) })), this.el.on("rb.resize", (function (t) { e.resize(t || window.event) })), this.el.on("lb.resize", (function (t) { e.resize(t || window.event) })), this.el.on("t.resize", (function (t) { e.resize(t || window.event) })), this.el.on("r.resize", (function (t) { e.resize(t || window.event) })), this.el.on("b.resize", (function (t) { e.resize(t || window.event) })), this.el.on("l.resize", (function (t) { e.resize(t || window.event) })), this.el.on("rot.resize", (function (t) { e.resize(t || window.event) })), this.el.on("point.resize", (function (t) { e.resize(t || window.event) })), this.update() } }, t.prototype.stop = function () { return this.el.off("lt.resize"), this.el.off("rt.resize"), this.el.off("rb.resize"), this.el.off("lb.resize"), this.el.off("t.resize"), this.el.off("r.resize"), this.el.off("b.resize"), this.el.off("l.resize"), this.el.off("rot.resize"), this.el.off("point.resize"), this }, t.prototype.resize = function (t) { var e = this; this.m = this.el.node.getScreenCTM().inverse(), this.offset = { x: window.pageXOffset, y: window.pageYOffset }; var i = this._extractPosition(t.detail.event); if (this.parameters = { type: this.el.type, p: this.transformPoint(i.x, i.y), x: t.detail.x, y: t.detail.y, box: this.el.bbox(), rotation: this.el.transform().rotation }, "text" === this.el.type && (this.parameters.fontSize = this.el.attr()["font-size"]), void 0 !== t.detail.i) { var a = this.el.array().valueOf(); this.parameters.i = t.detail.i, this.parameters.pointCoords = [a[t.detail.i][0], a[t.detail.i][1]] } switch (t.type) { case "lt": this.calc = function (t, e) { var i = this.snapToGrid(t, e); if (this.parameters.box.width - i[0] > 0 && this.parameters.box.height - i[1] > 0) { if ("text" === this.parameters.type) return this.el.move(this.parameters.box.x + i[0], this.parameters.box.y), void this.el.attr("font-size", this.parameters.fontSize - i[0]); i = this.checkAspectRatio(i), this.el.move(this.parameters.box.x + i[0], this.parameters.box.y + i[1]).size(this.parameters.box.width - i[0], this.parameters.box.height - i[1]) } }; break; case "rt": this.calc = function (t, e) { var i = this.snapToGrid(t, e, 2); if (this.parameters.box.width + i[0] > 0 && this.parameters.box.height - i[1] > 0) { if ("text" === this.parameters.type) return this.el.move(this.parameters.box.x - i[0], this.parameters.box.y), void this.el.attr("font-size", this.parameters.fontSize + i[0]); i = this.checkAspectRatio(i, !0), this.el.move(this.parameters.box.x, this.parameters.box.y + i[1]).size(this.parameters.box.width + i[0], this.parameters.box.height - i[1]) } }; break; case "rb": this.calc = function (t, e) { var i = this.snapToGrid(t, e, 0); if (this.parameters.box.width + i[0] > 0 && this.parameters.box.height + i[1] > 0) { if ("text" === this.parameters.type) return this.el.move(this.parameters.box.x - i[0], this.parameters.box.y), void this.el.attr("font-size", this.parameters.fontSize + i[0]); i = this.checkAspectRatio(i), this.el.move(this.parameters.box.x, this.parameters.box.y).size(this.parameters.box.width + i[0], this.parameters.box.height + i[1]) } }; break; case "lb": this.calc = function (t, e) { var i = this.snapToGrid(t, e, 1); if (this.parameters.box.width - i[0] > 0 && this.parameters.box.height + i[1] > 0) { if ("text" === this.parameters.type) return this.el.move(this.parameters.box.x + i[0], this.parameters.box.y), void this.el.attr("font-size", this.parameters.fontSize - i[0]); i = this.checkAspectRatio(i, !0), this.el.move(this.parameters.box.x + i[0], this.parameters.box.y).size(this.parameters.box.width - i[0], this.parameters.box.height + i[1]) } }; break; case "t": this.calc = function (t, e) { var i = this.snapToGrid(t, e, 2); if (this.parameters.box.height - i[1] > 0) { if ("text" === this.parameters.type) return; this.el.move(this.parameters.box.x, this.parameters.box.y + i[1]).height(this.parameters.box.height - i[1]) } }; break; case "r": this.calc = function (t, e) { var i = this.snapToGrid(t, e, 0); if (this.parameters.box.width + i[0] > 0) { if ("text" === this.parameters.type) return; this.el.move(this.parameters.box.x, this.parameters.box.y).width(this.parameters.box.width + i[0]) } }; break; case "b": this.calc = function (t, e) { var i = this.snapToGrid(t, e, 0); if (this.parameters.box.height + i[1] > 0) { if ("text" === this.parameters.type) return; this.el.move(this.parameters.box.x, this.parameters.box.y).height(this.parameters.box.height + i[1]) } }; break; case "l": this.calc = function (t, e) { var i = this.snapToGrid(t, e, 1); if (this.parameters.box.width - i[0] > 0) { if ("text" === this.parameters.type) return; this.el.move(this.parameters.box.x + i[0], this.parameters.box.y).width(this.parameters.box.width - i[0]) } }; break; case "rot": this.calc = function (t, e) { var i = t + this.parameters.p.x, a = e + this.parameters.p.y, s = Math.atan2(this.parameters.p.y - this.parameters.box.y - this.parameters.box.height / 2, this.parameters.p.x - this.parameters.box.x - this.parameters.box.width / 2), r = Math.atan2(a - this.parameters.box.y - this.parameters.box.height / 2, i - this.parameters.box.x - this.parameters.box.width / 2), o = this.parameters.rotation + 180 * (r - s) / Math.PI + this.options.snapToAngle / 2; this.el.center(this.parameters.box.cx, this.parameters.box.cy).rotate(o - o % this.options.snapToAngle, this.parameters.box.cx, this.parameters.box.cy) }; break; case "point": this.calc = function (t, e) { var i = this.snapToGrid(t, e, this.parameters.pointCoords[0], this.parameters.pointCoords[1]), a = this.el.array().valueOf(); a[this.parameters.i][0] = this.parameters.pointCoords[0] + i[0], a[this.parameters.i][1] = this.parameters.pointCoords[1] + i[1], this.el.plot(a) } }this.el.fire("resizestart", { dx: this.parameters.x, dy: this.parameters.y, event: t }), SVG.on(window, "touchmove.resize", (function (t) { e.update(t || window.event) })), SVG.on(window, "touchend.resize", (function () { e.done() })), SVG.on(window, "mousemove.resize", (function (t) { e.update(t || window.event) })), SVG.on(window, "mouseup.resize", (function () { e.done() })) }, t.prototype.update = function (t) { if (t) { var e = this._extractPosition(t), i = this.transformPoint(e.x, e.y), a = i.x - this.parameters.p.x, s = i.y - this.parameters.p.y; this.lastUpdateCall = [a, s], this.calc(a, s), this.el.fire("resizing", { dx: a, dy: s, event: t }) } else this.lastUpdateCall && this.calc(this.lastUpdateCall[0], this.lastUpdateCall[1]) }, t.prototype.done = function () { this.lastUpdateCall = null, SVG.off(window, "mousemove.resize"), SVG.off(window, "mouseup.resize"), SVG.off(window, "touchmove.resize"), SVG.off(window, "touchend.resize"), this.el.fire("resizedone") }, t.prototype.snapToGrid = function (t, e, i, a) { var s; return void 0 !== a ? s = [(i + t) % this.options.snapToGrid, (a + e) % this.options.snapToGrid] : (i = null == i ? 3 : i, s = [(this.parameters.box.x + t + (1 & i ? 0 : this.parameters.box.width)) % this.options.snapToGrid, (this.parameters.box.y + e + (2 & i ? 0 : this.parameters.box.height)) % this.options.snapToGrid]), t < 0 && (s[0] -= this.options.snapToGrid), e < 0 && (s[1] -= this.options.snapToGrid), t -= Math.abs(s[0]) < this.options.snapToGrid / 2 ? s[0] : s[0] - (t < 0 ? -this.options.snapToGrid : this.options.snapToGrid), e -= Math.abs(s[1]) < this.options.snapToGrid / 2 ? s[1] : s[1] - (e < 0 ? -this.options.snapToGrid : this.options.snapToGrid), this.constraintToBox(t, e, i, a) }, t.prototype.constraintToBox = function (t, e, i, a) { var s, r, o = this.options.constraint || {}; return void 0 !== a ? (s = i, r = a) : (s = this.parameters.box.x + (1 & i ? 0 : this.parameters.box.width), r = this.parameters.box.y + (2 & i ? 0 : this.parameters.box.height)), void 0 !== o.minX && s + t < o.minX && (t = o.minX - s), void 0 !== o.maxX && s + t > o.maxX && (t = o.maxX - s), void 0 !== o.minY && r + e < o.minY && (e = o.minY - r), void 0 !== o.maxY && r + e > o.maxY && (e = o.maxY - r), [t, e] }, t.prototype.checkAspectRatio = function (t, e) { if (!this.options.saveAspectRatio) return t; var i = t.slice(), a = this.parameters.box.width / this.parameters.box.height, s = this.parameters.box.width + t[0], r = this.parameters.box.height - t[1], o = s / r; return o < a ? (i[1] = s / a - this.parameters.box.height, e && (i[1] = -i[1])) : o > a && (i[0] = this.parameters.box.width - r * a, e && (i[0] = -i[0])), i }, SVG.extend(SVG.Element, { resize: function (e) { return (this.remember("_resizeHandler") || new t(this)).init(e || {}), this } }), SVG.Element.prototype.resize.defaults = { snapToAngle: .1, snapToGrid: 1, constraint: {}, saveAspectRatio: !1 } }).call(this) }(), void 0 === window.Apex && (window.Apex = {}); var Gt = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w } return r(t, [{ key: "initModules", value: function () { this.ctx.publicMethods = ["updateOptions", "updateSeries", "appendData", "appendSeries", "isSeriesHidden", "toggleSeries", "showSeries", "hideSeries", "setLocale", "resetSeries", "zoomX", "toggleDataPointSelection", "dataURI", "exportToCSV", "addXaxisAnnotation", "addYaxisAnnotation", "addPointAnnotation", "clearAnnotations", "removeAnnotation", "paper", "destroy"], this.ctx.eventList = ["click", "mousedown", "mousemove", "mouseleave", "touchstart", "touchmove", "touchleave", "mouseup", "touchend"], this.ctx.animations = new b(this.ctx), this.ctx.axes = new J(this.ctx), this.ctx.core = new Wt(this.ctx.el, this.ctx), this.ctx.config = new Y({}), this.ctx.data = new B(this.ctx), this.ctx.grid = new j(this.ctx), this.ctx.graphics = new m(this.ctx), this.ctx.coreUtils = new y(this.ctx), this.ctx.crosshairs = new Q(this.ctx), this.ctx.events = new Z(this.ctx), this.ctx.exports = new G(this.ctx), this.ctx.localization = new $(this.ctx), this.ctx.options = new I, this.ctx.responsive = new K(this.ctx), this.ctx.series = new W(this.ctx), this.ctx.theme = new tt(this.ctx), this.ctx.formatters = new S(this.ctx), this.ctx.titleSubtitle = new et(this.ctx), this.ctx.legend = new lt(this.ctx), this.ctx.toolbar = new ht(this.ctx), this.ctx.tooltip = new bt(this.ctx), this.ctx.dimensions = new ot(this.ctx), this.ctx.updateHelpers = new Bt(this.ctx), this.ctx.zoomPanSelection = new ct(this.ctx), this.ctx.w.globals.tooltip = new bt(this.ctx) } }]), t }(), Vt = function () { function t(e) { a(this, t), this.ctx = e, this.w = e.w } return r(t, [{ key: "clear", value: function (t) { var e = t.isUpdating; this.ctx.zoomPanSelection && this.ctx.zoomPanSelection.destroy(), this.ctx.toolbar && this.ctx.toolbar.destroy(), this.ctx.animations = null, this.ctx.axes = null, this.ctx.annotations = null, this.ctx.core = null, this.ctx.data = null, this.ctx.grid = null, this.ctx.series = null, this.ctx.responsive = null, this.ctx.theme = null, this.ctx.formatters = null, this.ctx.titleSubtitle = null, this.ctx.legend = null, this.ctx.dimensions = null, this.ctx.options = null, this.ctx.crosshairs = null, this.ctx.zoomPanSelection = null, this.ctx.updateHelpers = null, this.ctx.toolbar = null, this.ctx.localization = null, this.ctx.w.globals.tooltip = null, this.clearDomElements({ isUpdating: e }) } }, { key: "killSVG", value: function (t) { t.each((function (t, e) { this.removeClass("*"), this.off(), this.stop() }), !0), t.ungroup(), t.clear() } }, { key: "clearDomElements", value: function (t) { var e = this, i = t.isUpdating, a = this.w.globals.dom.Paper.node; a.parentNode && a.parentNode.parentNode && !i && (a.parentNode.parentNode.style.minHeight = "unset"); var s = this.w.globals.dom.baseEl; s && this.ctx.eventList.forEach((function (t) { s.removeEventListener(t, e.ctx.events.documentEvent) })); var r = this.w.globals.dom; if (null !== this.ctx.el) for (; this.ctx.el.firstChild;)this.ctx.el.removeChild(this.ctx.el.firstChild); this.killSVG(r.Paper), r.Paper.remove(), r.elWrap = null, r.elGraphical = null, r.elLegendWrap = null, r.elLegendForeign = null, r.baseEl = null, r.elGridRect = null, r.elGridRectMask = null, r.elGridRectMarkerMask = null, r.elForecastMask = null, r.elNonForecastMask = null, r.elDefs = null } }]), t }(), jt = new WeakMap; var _t = function () { function t(e, i) { a(this, t), this.opts = i, this.ctx = this, this.w = new R(i).init(), this.el = e, this.w.globals.cuid = x.randomId(), this.w.globals.chartID = this.w.config.chart.id ? x.escapeString(this.w.config.chart.id) : this.w.globals.cuid, new Gt(this).initModules(), this.create = x.bind(this.create, this), this.windowResizeHandler = this._windowResizeHandler.bind(this), this.parentResizeHandler = this._parentResizeCallback.bind(this) } return r(t, [{ key: "render", value: function () { var t = this; return new Promise((function (e, i) { if (null !== t.el) { void 0 === Apex._chartInstances && (Apex._chartInstances = []), t.w.config.chart.id && Apex._chartInstances.push({ id: t.w.globals.chartID, group: t.w.config.chart.group, chart: t }), t.setLocale(t.w.config.chart.defaultLocale); var a = t.w.config.chart.events.beforeMount; "function" == typeof a && a(t, t.w), t.events.fireEvent("beforeMount", [t, t.w]), window.addEventListener("resize", t.windowResizeHandler), function (t, e) { var i = !1; if (t.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) { var a = t.getBoundingClientRect(); "none" !== t.style.display && 0 !== a.width || (i = !0) } var s = new ResizeObserver((function (a) { i && e.call(t, a), i = !0 })); t.nodeType === Node.DOCUMENT_FRAGMENT_NODE ? Array.from(t.children).forEach((function (t) { return s.observe(t) })) : s.observe(t), jt.set(e, s) }(t.el.parentNode, t.parentResizeHandler); var s = t.el.getRootNode && t.el.getRootNode(), r = x.is("ShadowRoot", s), o = t.el.ownerDocument, n = r ? s.getElementById("apexcharts-css") : o.getElementById("apexcharts-css"); if (!n) { var l; (n = document.createElement("style")).id = "apexcharts-css", n.textContent = '@keyframes opaque {\n 0% {\n opacity: 0\n }\n\n to {\n opacity: 1\n }\n}\n\n@keyframes resizeanim {\n 0%,to {\n opacity: 0\n }\n}\n\n.apexcharts-canvas {\n position: relative;\n user-select: none\n}\n\n.apexcharts-canvas ::-webkit-scrollbar {\n -webkit-appearance: none;\n width: 6px\n}\n\n.apexcharts-canvas ::-webkit-scrollbar-thumb {\n border-radius: 4px;\n background-color: rgba(0,0,0,.5);\n box-shadow: 0 0 1px rgba(255,255,255,.5);\n -webkit-box-shadow: 0 0 1px rgba(255,255,255,.5)\n}\n\n.apexcharts-inner {\n position: relative\n}\n\n.apexcharts-text tspan {\n font-family: inherit\n}\n\n.legend-mouseover-inactive {\n transition: .15s ease all;\n opacity: .2\n}\n\n.apexcharts-legend-text {\n padding-left: 15px;\n margin-left: -15px;\n}\n\n.apexcharts-series-collapsed {\n opacity: 0\n}\n\n.apexcharts-tooltip {\n border-radius: 5px;\n box-shadow: 2px 2px 6px -4px #999;\n cursor: default;\n font-size: 14px;\n left: 62px;\n opacity: 0;\n pointer-events: none;\n position: absolute;\n top: 20px;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n white-space: nowrap;\n z-index: 12;\n transition: .15s ease all\n}\n\n.apexcharts-tooltip.apexcharts-active {\n opacity: 1;\n transition: .15s ease all\n}\n\n.apexcharts-tooltip.apexcharts-theme-light {\n border: 1px solid #e3e3e3;\n background: rgba(255,255,255,.96)\n}\n\n.apexcharts-tooltip.apexcharts-theme-dark {\n color: #fff;\n background: rgba(30,30,30,.8)\n}\n\n.apexcharts-tooltip * {\n font-family: inherit\n}\n\n.apexcharts-tooltip-title {\n padding: 6px;\n font-size: 15px;\n margin-bottom: 4px\n}\n\n.apexcharts-tooltip.apexcharts-theme-light .apexcharts-tooltip-title {\n background: #eceff1;\n border-bottom: 1px solid #ddd\n}\n\n.apexcharts-tooltip.apexcharts-theme-dark .apexcharts-tooltip-title {\n background: rgba(0,0,0,.7);\n border-bottom: 1px solid #333\n}\n\n.apexcharts-tooltip-text-goals-value,.apexcharts-tooltip-text-y-value,.apexcharts-tooltip-text-z-value {\n display: inline-block;\n margin-left: 5px;\n font-weight: 600\n}\n\n.apexcharts-tooltip-text-goals-label:empty,.apexcharts-tooltip-text-goals-value:empty,.apexcharts-tooltip-text-y-label:empty,.apexcharts-tooltip-text-y-value:empty,.apexcharts-tooltip-text-z-value:empty,.apexcharts-tooltip-title:empty {\n display: none\n}\n\n.apexcharts-tooltip-text-goals-label,.apexcharts-tooltip-text-goals-value {\n padding: 6px 0 5px\n}\n\n.apexcharts-tooltip-goals-group,.apexcharts-tooltip-text-goals-label,.apexcharts-tooltip-text-goals-value {\n display: flex\n}\n\n.apexcharts-tooltip-text-goals-label:not(:empty),.apexcharts-tooltip-text-goals-value:not(:empty) {\n margin-top: -6px\n}\n\n.apexcharts-tooltip-marker {\n width: 12px;\n height: 12px;\n position: relative;\n top: 0;\n margin-right: 10px;\n border-radius: 50%\n}\n\n.apexcharts-tooltip-series-group {\n padding: 0 10px;\n display: none;\n text-align: left;\n justify-content: left;\n align-items: center\n}\n\n.apexcharts-tooltip-series-group.apexcharts-active .apexcharts-tooltip-marker {\n opacity: 1\n}\n\n.apexcharts-tooltip-series-group.apexcharts-active,.apexcharts-tooltip-series-group:last-child {\n padding-bottom: 4px\n}\n\n.apexcharts-tooltip-series-group-hidden {\n opacity: 0;\n height: 0;\n line-height: 0;\n padding: 0!important\n}\n\n.apexcharts-tooltip-y-group {\n padding: 6px 0 5px\n}\n\n.apexcharts-custom-tooltip,.apexcharts-tooltip-box {\n padding: 4px 8px\n}\n\n.apexcharts-tooltip-boxPlot {\n display: flex;\n flex-direction: column-reverse\n}\n\n.apexcharts-tooltip-box>div {\n margin: 4px 0\n}\n\n.apexcharts-tooltip-box span.value {\n font-weight: 700\n}\n\n.apexcharts-tooltip-rangebar {\n padding: 5px 8px\n}\n\n.apexcharts-tooltip-rangebar .category {\n font-weight: 600;\n color: #777\n}\n\n.apexcharts-tooltip-rangebar .series-name {\n font-weight: 700;\n display: block;\n margin-bottom: 5px\n}\n\n.apexcharts-xaxistooltip,.apexcharts-yaxistooltip {\n opacity: 0;\n pointer-events: none;\n color: #373d3f;\n font-size: 13px;\n text-align: center;\n border-radius: 2px;\n position: absolute;\n z-index: 10;\n background: #eceff1;\n border: 1px solid #90a4ae\n}\n\n.apexcharts-xaxistooltip {\n padding: 9px 10px;\n transition: .15s ease all\n}\n\n.apexcharts-xaxistooltip.apexcharts-theme-dark {\n background: rgba(0,0,0,.7);\n border: 1px solid rgba(0,0,0,.5);\n color: #fff\n}\n\n.apexcharts-xaxistooltip:after,.apexcharts-xaxistooltip:before {\n left: 50%;\n border: solid transparent;\n content: " ";\n height: 0;\n width: 0;\n position: absolute;\n pointer-events: none\n}\n\n.apexcharts-xaxistooltip:after {\n border-color: transparent;\n border-width: 6px;\n margin-left: -6px\n}\n\n.apexcharts-xaxistooltip:before {\n border-color: transparent;\n border-width: 7px;\n margin-left: -7px\n}\n\n.apexcharts-xaxistooltip-bottom:after,.apexcharts-xaxistooltip-bottom:before {\n bottom: 100%\n}\n\n.apexcharts-xaxistooltip-top:after,.apexcharts-xaxistooltip-top:before {\n top: 100%\n}\n\n.apexcharts-xaxistooltip-bottom:after {\n border-bottom-color: #eceff1\n}\n\n.apexcharts-xaxistooltip-bottom:before {\n border-bottom-color: #90a4ae\n}\n\n.apexcharts-xaxistooltip-bottom.apexcharts-theme-dark:after,.apexcharts-xaxistooltip-bottom.apexcharts-theme-dark:before {\n border-bottom-color: rgba(0,0,0,.5)\n}\n\n.apexcharts-xaxistooltip-top:after {\n border-top-color: #eceff1\n}\n\n.apexcharts-xaxistooltip-top:before {\n border-top-color: #90a4ae\n}\n\n.apexcharts-xaxistooltip-top.apexcharts-theme-dark:after,.apexcharts-xaxistooltip-top.apexcharts-theme-dark:before {\n border-top-color: rgba(0,0,0,.5)\n}\n\n.apexcharts-xaxistooltip.apexcharts-active {\n opacity: 1;\n transition: .15s ease all\n}\n\n.apexcharts-yaxistooltip {\n padding: 4px 10px\n}\n\n.apexcharts-yaxistooltip.apexcharts-theme-dark {\n background: rgba(0,0,0,.7);\n border: 1px solid rgba(0,0,0,.5);\n color: #fff\n}\n\n.apexcharts-yaxistooltip:after,.apexcharts-yaxistooltip:before {\n top: 50%;\n border: solid transparent;\n content: " ";\n height: 0;\n width: 0;\n position: absolute;\n pointer-events: none\n}\n\n.apexcharts-yaxistooltip:after {\n border-color: transparent;\n border-width: 6px;\n margin-top: -6px\n}\n\n.apexcharts-yaxistooltip:before {\n border-color: transparent;\n border-width: 7px;\n margin-top: -7px\n}\n\n.apexcharts-yaxistooltip-left:after,.apexcharts-yaxistooltip-left:before {\n left: 100%\n}\n\n.apexcharts-yaxistooltip-right:after,.apexcharts-yaxistooltip-right:before {\n right: 100%\n}\n\n.apexcharts-yaxistooltip-left:after {\n border-left-color: #eceff1\n}\n\n.apexcharts-yaxistooltip-left:before {\n border-left-color: #90a4ae\n}\n\n.apexcharts-yaxistooltip-left.apexcharts-theme-dark:after,.apexcharts-yaxistooltip-left.apexcharts-theme-dark:before {\n border-left-color: rgba(0,0,0,.5)\n}\n\n.apexcharts-yaxistooltip-right:after {\n border-right-color: #eceff1\n}\n\n.apexcharts-yaxistooltip-right:before {\n border-right-color: #90a4ae\n}\n\n.apexcharts-yaxistooltip-right.apexcharts-theme-dark:after,.apexcharts-yaxistooltip-right.apexcharts-theme-dark:before {\n border-right-color: rgba(0,0,0,.5)\n}\n\n.apexcharts-yaxistooltip.apexcharts-active {\n opacity: 1\n}\n\n.apexcharts-yaxistooltip-hidden {\n display: none\n}\n\n.apexcharts-xcrosshairs,.apexcharts-ycrosshairs {\n pointer-events: none;\n opacity: 0;\n transition: .15s ease all\n}\n\n.apexcharts-xcrosshairs.apexcharts-active,.apexcharts-ycrosshairs.apexcharts-active {\n opacity: 1;\n transition: .15s ease all\n}\n\n.apexcharts-ycrosshairs-hidden {\n opacity: 0\n}\n\n.apexcharts-selection-rect {\n cursor: move\n}\n\n.svg_select_boundingRect,.svg_select_points_rot {\n pointer-events: none;\n opacity: 0;\n visibility: hidden\n}\n\n.apexcharts-selection-rect+g .svg_select_boundingRect,.apexcharts-selection-rect+g .svg_select_points_rot {\n opacity: 0;\n visibility: hidden\n}\n\n.apexcharts-selection-rect+g .svg_select_points_l,.apexcharts-selection-rect+g .svg_select_points_r {\n cursor: ew-resize;\n opacity: 1;\n visibility: visible\n}\n\n.svg_select_points {\n fill: #efefef;\n stroke: #333;\n rx: 2\n}\n\n.apexcharts-svg.apexcharts-zoomable.hovering-zoom {\n cursor: crosshair\n}\n\n.apexcharts-svg.apexcharts-zoomable.hovering-pan {\n cursor: move\n}\n\n.apexcharts-menu-icon,.apexcharts-pan-icon,.apexcharts-reset-icon,.apexcharts-selection-icon,.apexcharts-toolbar-custom-icon,.apexcharts-zoom-icon,.apexcharts-zoomin-icon,.apexcharts-zoomout-icon {\n cursor: pointer;\n width: 20px;\n height: 20px;\n line-height: 24px;\n color: #6e8192;\n text-align: center\n}\n\n.apexcharts-menu-icon svg,.apexcharts-reset-icon svg,.apexcharts-zoom-icon svg,.apexcharts-zoomin-icon svg,.apexcharts-zoomout-icon svg {\n fill: #6e8192\n}\n\n.apexcharts-selection-icon svg {\n fill: #444;\n transform: scale(.76)\n}\n\n.apexcharts-theme-dark .apexcharts-menu-icon svg,.apexcharts-theme-dark .apexcharts-pan-icon svg,.apexcharts-theme-dark .apexcharts-reset-icon svg,.apexcharts-theme-dark .apexcharts-selection-icon svg,.apexcharts-theme-dark .apexcharts-toolbar-custom-icon svg,.apexcharts-theme-dark .apexcharts-zoom-icon svg,.apexcharts-theme-dark .apexcharts-zoomin-icon svg,.apexcharts-theme-dark .apexcharts-zoomout-icon svg {\n fill: #f3f4f5\n}\n\n.apexcharts-canvas .apexcharts-reset-zoom-icon.apexcharts-selected svg,.apexcharts-canvas .apexcharts-selection-icon.apexcharts-selected svg,.apexcharts-canvas .apexcharts-zoom-icon.apexcharts-selected svg {\n fill: #008ffb\n}\n\n.apexcharts-theme-light .apexcharts-menu-icon:hover svg,.apexcharts-theme-light .apexcharts-reset-icon:hover svg,.apexcharts-theme-light .apexcharts-selection-icon:not(.apexcharts-selected):hover svg,.apexcharts-theme-light .apexcharts-zoom-icon:not(.apexcharts-selected):hover svg,.apexcharts-theme-light .apexcharts-zoomin-icon:hover svg,.apexcharts-theme-light .apexcharts-zoomout-icon:hover svg {\n fill: #333\n}\n\n.apexcharts-menu-icon,.apexcharts-selection-icon {\n position: relative\n}\n\n.apexcharts-reset-icon {\n margin-left: 5px\n}\n\n.apexcharts-menu-icon,.apexcharts-reset-icon,.apexcharts-zoom-icon {\n transform: scale(.85)\n}\n\n.apexcharts-zoomin-icon,.apexcharts-zoomout-icon {\n transform: scale(.7)\n}\n\n.apexcharts-zoomout-icon {\n margin-right: 3px\n}\n\n.apexcharts-pan-icon {\n transform: scale(.62);\n position: relative;\n left: 1px;\n top: 0\n}\n\n.apexcharts-pan-icon svg {\n fill: #fff;\n stroke: #6e8192;\n stroke-width: 2\n}\n\n.apexcharts-pan-icon.apexcharts-selected svg {\n stroke: #008ffb\n}\n\n.apexcharts-pan-icon:not(.apexcharts-selected):hover svg {\n stroke: #333\n}\n\n.apexcharts-toolbar {\n position: absolute;\n z-index: 11;\n max-width: 176px;\n text-align: right;\n border-radius: 3px;\n padding: 0 6px 2px;\n display: flex;\n justify-content: space-between;\n align-items: center\n}\n\n.apexcharts-menu {\n background: #fff;\n position: absolute;\n top: 100%;\n border: 1px solid #ddd;\n border-radius: 3px;\n padding: 3px;\n right: 10px;\n opacity: 0;\n min-width: 110px;\n transition: .15s ease all;\n pointer-events: none\n}\n\n.apexcharts-menu.apexcharts-menu-open {\n opacity: 1;\n pointer-events: all;\n transition: .15s ease all\n}\n\n.apexcharts-menu-item {\n padding: 6px 7px;\n font-size: 12px;\n cursor: pointer\n}\n\n.apexcharts-theme-light .apexcharts-menu-item:hover {\n background: #eee\n}\n\n.apexcharts-theme-dark .apexcharts-menu {\n background: rgba(0,0,0,.7);\n color: #fff\n}\n\n@media screen and (min-width:768px) {\n .apexcharts-canvas:hover .apexcharts-toolbar {\n opacity: 1\n }\n}\n\n.apexcharts-canvas .apexcharts-element-hidden,.apexcharts-datalabel.apexcharts-element-hidden,.apexcharts-hide .apexcharts-series-points {\n display: none;\n}\n\n.apexcharts-hidden-element-shown {\n opacity: 1;\n transition: 0.25s ease all;\n}\n.apexcharts-datalabel,.apexcharts-datalabel-label,.apexcharts-datalabel-value,.apexcharts-datalabels,.apexcharts-pie-label {\n cursor: default;\n pointer-events: none\n}\n\n.apexcharts-pie-label-delay {\n opacity: 0;\n animation-name: opaque;\n animation-duration: .3s;\n animation-fill-mode: forwards;\n animation-timing-function: ease\n}\n\n.apexcharts-radialbar-label {\n cursor: pointer;\n}\n\n.apexcharts-annotation-rect,.apexcharts-area-series .apexcharts-area,.apexcharts-area-series .apexcharts-series-markers .apexcharts-marker.no-pointer-events,.apexcharts-gridline,.apexcharts-line,.apexcharts-line-series .apexcharts-series-markers .apexcharts-marker.no-pointer-events,.apexcharts-point-annotation-label,.apexcharts-radar-series path,.apexcharts-radar-series polygon,.apexcharts-toolbar svg,.apexcharts-tooltip .apexcharts-marker,.apexcharts-xaxis-annotation-label,.apexcharts-yaxis-annotation-label,.apexcharts-zoom-rect {\n pointer-events: none\n}\n\n.apexcharts-marker {\n transition: .15s ease all\n}\n\n.resize-triggers {\n animation: 1ms resizeanim;\n visibility: hidden;\n opacity: 0;\n height: 100%;\n width: 100%;\n overflow: hidden\n}\n\n.contract-trigger:before,.resize-triggers,.resize-triggers>div {\n content: " ";\n display: block;\n position: absolute;\n top: 0;\n left: 0\n}\n\n.resize-triggers>div {\n height: 100%;\n width: 100%;\n background: #eee;\n overflow: auto\n}\n\n.contract-trigger:before {\n overflow: hidden;\n width: 200%;\n height: 200%\n}\n\n.apexcharts-bar-goals-markers{\n pointer-events: none\n}\n\n.apexcharts-bar-shadows{\n pointer-events: none\n}\n\n.apexcharts-rangebar-goals-markers{\n pointer-events: none\n}'; var h = (null === (l = t.opts.chart) || void 0 === l ? void 0 : l.nonce) || t.w.config.chart.nonce; h && n.setAttribute("nonce", h), r ? s.prepend(n) : o.head.appendChild(n) } var c = t.create(t.w.config.series, {}); if (!c) return e(t); t.mount(c).then((function () { "function" == typeof t.w.config.chart.events.mounted && t.w.config.chart.events.mounted(t, t.w), t.events.fireEvent("mounted", [t, t.w]), e(c) })).catch((function (t) { i(t) })) } else i(new Error("Element not found")) })) } }, { key: "create", value: function (t, e) { var i = this.w; new Gt(this).initModules(); var a = this.w.globals; (a.noData = !1, a.animationEnded = !1, this.responsive.checkResponsiveConfig(e), i.config.xaxis.convertedCatToNumeric) && new E(i.config).convertCatToNumericXaxis(i.config, this.ctx); if (null === this.el) return a.animationEnded = !0, null; if (this.core.setupElements(), "treemap" === i.config.chart.type && (i.config.grid.show = !1, i.config.yaxis[0].show = !1), 0 === a.svgWidth) return a.animationEnded = !0, null; var s = y.checkComboSeries(t, i.config.chart.type); a.comboCharts = s.comboCharts, a.comboBarCount = s.comboBarCount; var r = t.every((function (t) { return t.data && 0 === t.data.length })); (0 === t.length || r && a.collapsedSeries.length < 1) && this.series.handleNoData(), this.events.setupEventHandlers(), this.data.parseData(t), this.theme.init(), new D(this).setGlobalMarkerSize(), this.formatters.setLabelFormatters(), this.titleSubtitle.draw(), a.noData && a.collapsedSeries.length !== a.series.length && !i.config.legend.showForSingleSeries || this.legend.init(), this.series.hasAllSeriesEqualX(), a.axisCharts && (this.core.coreCalculations(), "category" !== i.config.xaxis.type && this.formatters.setLabelFormatters(), this.ctx.toolbar.minX = i.globals.minX, this.ctx.toolbar.maxX = i.globals.maxX), this.formatters.heatmapLabelFormatters(), new y(this).getLargestMarkerSize(), this.dimensions.plotCoords(); var o = this.core.xySettings(); this.grid.createGridMask(); var n = this.core.plotChartType(t, o), l = new N(this); return l.bringForward(), i.config.dataLabels.background.enabled && l.dataLabelsBackground(), this.core.shiftGraphPosition(), { elGraph: n, xyRatios: o, dimensions: { plot: { left: i.globals.translateX, top: i.globals.translateY, width: i.globals.gridWidth, height: i.globals.gridHeight } } } } }, { key: "mount", value: function () { var t = this, e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : null, i = this, a = i.w; return new Promise((function (s, r) { if (null === i.el) return r(new Error("Not enough data to display or target element not found")); (null === e || a.globals.allSeriesCollapsed) && i.series.handleNoData(), i.grid = new j(i); var o, n, l = i.grid.drawGrid(); (i.annotations = new T(i), i.annotations.drawImageAnnos(), i.annotations.drawTextAnnos(), "back" === a.config.grid.position) && (l && a.globals.dom.elGraphical.add(l.el), null != l && null !== (o = l.elGridBorders) && void 0 !== o && o.node && a.globals.dom.elGraphical.add(l.elGridBorders)); if (Array.isArray(e.elGraph)) for (var h = 0; h < e.elGraph.length; h++)a.globals.dom.elGraphical.add(e.elGraph[h]); else a.globals.dom.elGraphical.add(e.elGraph); "front" === a.config.grid.position && (l && a.globals.dom.elGraphical.add(l.el), null != l && null !== (n = l.elGridBorders) && void 0 !== n && n.node && a.globals.dom.elGraphical.add(l.elGridBorders)); "front" === a.config.xaxis.crosshairs.position && i.crosshairs.drawXCrosshairs(), "front" === a.config.yaxis[0].crosshairs.position && i.crosshairs.drawYCrosshairs(), "treemap" !== a.config.chart.type && i.axes.drawAxis(a.config.chart.type, l); var c = new V(t.ctx, l), d = new q(t.ctx, l); if (null !== l && (c.xAxisLabelCorrections(l.xAxisTickWidth), d.setYAxisTextAlignments(), a.config.yaxis.map((function (t, e) { -1 === a.globals.ignoreYAxisIndexes.indexOf(e) && d.yAxisTitleRotate(e, t.opposite) }))), i.annotations.drawAxesAnnotations(), !a.globals.noData) { if (a.config.tooltip.enabled && !a.globals.noData && i.w.globals.tooltip.drawTooltip(e.xyRatios), a.globals.axisCharts && (a.globals.isXNumeric || a.config.xaxis.convertedCatToNumeric || a.globals.isRangeBar)) (a.config.chart.zoom.enabled || a.config.chart.selection && a.config.chart.selection.enabled || a.config.chart.pan && a.config.chart.pan.enabled) && i.zoomPanSelection.init({ xyRatios: e.xyRatios }); else { var g = a.config.chart.toolbar.tools;["zoom", "zoomin", "zoomout", "selection", "pan", "reset"].forEach((function (t) { g[t] = !1 })) } a.config.chart.toolbar.show && !a.globals.allSeriesCollapsed && i.toolbar.createToolbar() } a.globals.memory.methodsToExec.length > 0 && a.globals.memory.methodsToExec.forEach((function (t) { t.method(t.params, !1, t.context) })), a.globals.axisCharts || a.globals.noData || i.core.resizeNonAxisCharts(), s(i) })) } }, { key: "destroy", value: function () { var t, e; window.removeEventListener("resize", this.windowResizeHandler), this.el.parentNode, t = this.parentResizeHandler, (e = jt.get(t)) && (e.disconnect(), jt.delete(t)); var i = this.w.config.chart.id; i && Apex._chartInstances.forEach((function (t, e) { t.id === x.escapeString(i) && Apex._chartInstances.splice(e, 1) })), new Vt(this.ctx).clear({ isUpdating: !1 }) } }, { key: "updateOptions", value: function (t) { var e = this, i = arguments.length > 1 && void 0 !== arguments[1] && arguments[1], a = !(arguments.length > 2 && void 0 !== arguments[2]) || arguments[2], s = !(arguments.length > 3 && void 0 !== arguments[3]) || arguments[3], r = !(arguments.length > 4 && void 0 !== arguments[4]) || arguments[4], o = this.w; return o.globals.selection = void 0, t.series && (this.series.resetSeries(!1, !0, !1), t.series.length && t.series[0].data && (t.series = t.series.map((function (t, i) { return e.updateHelpers._extendSeries(t, i) }))), this.updateHelpers.revertDefaultAxisMinMax()), t.xaxis && (t = this.updateHelpers.forceXAxisUpdate(t)), t.yaxis && (t = this.updateHelpers.forceYAxisUpdate(t)), o.globals.collapsedSeriesIndices.length > 0 && this.series.clearPreviousPaths(), t.theme && (t = this.theme.updateThemeOptions(t)), this.updateHelpers._updateOptions(t, i, a, s, r) } }, { key: "updateSeries", value: function () { var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : [], e = !(arguments.length > 1 && void 0 !== arguments[1]) || arguments[1], i = !(arguments.length > 2 && void 0 !== arguments[2]) || arguments[2]; return this.series.resetSeries(!1), this.updateHelpers.revertDefaultAxisMinMax(), this.updateHelpers._updateSeries(t, e, i) } }, { key: "appendSeries", value: function (t) { var e = !(arguments.length > 1 && void 0 !== arguments[1]) || arguments[1], i = !(arguments.length > 2 && void 0 !== arguments[2]) || arguments[2], a = this.w.config.series.slice(); return a.push(t), this.series.resetSeries(!1), this.updateHelpers.revertDefaultAxisMinMax(), this.updateHelpers._updateSeries(a, e, i) } }, { key: "appendData", value: function (t) { var e = !(arguments.length > 1 && void 0 !== arguments[1]) || arguments[1], i = this; i.w.globals.dataChanged = !0, i.series.getPreviousPaths(); for (var a = i.w.config.series.slice(), s = 0; s < a.length; s++)if (null !== t[s] && void 0 !== t[s]) for (var r = 0; r < t[s].data.length; r++)a[s].data.push(t[s].data[r]); return i.w.config.series = a, e && (i.w.globals.initialSeries = x.clone(i.w.config.series)), this.update() } }, { key: "update", value: function (t) { var e = this; return new Promise((function (i, a) { new Vt(e.ctx).clear({ isUpdating: !0 }); var s = e.create(e.w.config.series, t); if (!s) return i(e); e.mount(s).then((function () { "function" == typeof e.w.config.chart.events.updated && e.w.config.chart.events.updated(e, e.w), e.events.fireEvent("updated", [e, e.w]), e.w.globals.isDirty = !0, i(e) })).catch((function (t) { a(t) })) })) } }, { key: "getSyncedCharts", value: function () { var t = this.getGroupedCharts(), e = [this]; return t.length && (e = [], t.forEach((function (t) { e.push(t) }))), e } }, { key: "getGroupedCharts", value: function () { var t = this; return Apex._chartInstances.filter((function (t) { if (t.group) return !0 })).map((function (e) { return t.w.config.chart.group === e.group ? e.chart : t })) } }, { key: "toggleSeries", value: function (t) { return this.series.toggleSeries(t) } }, { key: "highlightSeriesOnLegendHover", value: function (t, e) { return this.series.toggleSeriesOnHover(t, e) } }, { key: "showSeries", value: function (t) { this.series.showSeries(t) } }, { key: "hideSeries", value: function (t) { this.series.hideSeries(t) } }, { key: "isSeriesHidden", value: function (t) { this.series.isSeriesHidden(t) } }, { key: "resetSeries", value: function () { var t = !(arguments.length > 0 && void 0 !== arguments[0]) || arguments[0], e = !(arguments.length > 1 && void 0 !== arguments[1]) || arguments[1]; this.series.resetSeries(t, e) } }, { key: "addEventListener", value: function (t, e) { this.events.addEventListener(t, e) } }, { key: "removeEventListener", value: function (t, e) { this.events.removeEventListener(t, e) } }, { key: "addXaxisAnnotation", value: function (t) { var e = !(arguments.length > 1 && void 0 !== arguments[1]) || arguments[1], i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : void 0, a = this; i && (a = i), a.annotations.addXaxisAnnotationExternal(t, e, a) } }, { key: "addYaxisAnnotation", value: function (t) { var e = !(arguments.length > 1 && void 0 !== arguments[1]) || arguments[1], i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : void 0, a = this; i && (a = i), a.annotations.addYaxisAnnotationExternal(t, e, a) } }, { key: "addPointAnnotation", value: function (t) { var e = !(arguments.length > 1 && void 0 !== arguments[1]) || arguments[1], i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : void 0, a = this; i && (a = i), a.annotations.addPointAnnotationExternal(t, e, a) } }, { key: "clearAnnotations", value: function () { var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : void 0, e = this; t && (e = t), e.annotations.clearAnnotations(e) } }, { key: "removeAnnotation", value: function (t) { var e = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : void 0, i = this; e && (i = e), i.annotations.removeAnnotation(i, t) } }, { key: "getChartArea", value: function () { return this.w.globals.dom.baseEl.querySelector(".apexcharts-inner") } }, { key: "getSeriesTotalXRange", value: function (t, e) { return this.coreUtils.getSeriesTotalsXRange(t, e) } }, { key: "getHighestValueInSeries", value: function () { var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 0; return new U(this.ctx).getMinYMaxY(t).highestY } }, { key: "getLowestValueInSeries", value: function () { var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 0; return new U(this.ctx).getMinYMaxY(t).lowestY } }, { key: "getSeriesTotal", value: function () { return this.w.globals.seriesTotals } }, { key: "toggleDataPointSelection", value: function (t, e) { return this.updateHelpers.toggleDataPointSelection(t, e) } }, { key: "zoomX", value: function (t, e) { this.ctx.toolbar.zoomUpdateOptions(t, e) } }, { key: "setLocale", value: function (t) { this.localization.setCurrentLocaleValues(t) } }, { key: "dataURI", value: function (t) { return new G(this.ctx).dataURI(t) } }, { key: "exportToCSV", value: function () { var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}; return new G(this.ctx).exportToCSV(t) } }, { key: "paper", value: function () { return this.w.globals.dom.Paper } }, { key: "_parentResizeCallback", value: function () { this.w.globals.animationEnded && this.w.config.chart.redrawOnParentResize && this._windowResize() } }, { key: "_windowResize", value: function () { var t = this; clearTimeout(this.w.globals.resizeTimer), this.w.globals.resizeTimer = window.setTimeout((function () { t.w.globals.resized = !0, t.w.globals.dataChanged = !1, t.ctx.update() }), 150) } }, { key: "_windowResizeHandler", value: function () { var t = this.w.config.chart.redrawOnWindowResize; "function" == typeof t && (t = t()), t && this._windowResize() } }], [{ key: "getChartByID", value: function (t) { var e = x.escapeString(t); if (Apex._chartInstances) { var i = Apex._chartInstances.filter((function (t) { return t.id === e }))[0]; return i && i.chart } } }, { key: "initOnLoad", value: function () { for (var e = document.querySelectorAll("[data-apexcharts]"), i = 0; i < e.length; i++) { new t(e[i], JSON.parse(e[i].getAttribute("data-options"))).render() } } }, { key: "exec", value: function (t, e) { var i = this.getChartByID(t); if (i) { i.w.globals.isExecCalled = !0; var a = null; if (-1 !== i.publicMethods.indexOf(e)) { for (var s = arguments.length, r = new Array(s > 2 ? s - 2 : 0), o = 2; o < s; o++)r[o - 2] = arguments[o]; a = i[e].apply(i, r) } return a } } }, { key: "merge", value: function (t, e) { return x.extend(t, e) } }]), t }(); return _t +})); diff --git a/public/svgs/affine.svg b/public/svgs/affine.svg new file mode 100644 index 000000000..d8063e920 --- /dev/null +++ b/public/svgs/affine.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/svgs/anythingllm.svg b/public/svgs/anythingllm.svg new file mode 100644 index 000000000..1c25f8711 --- /dev/null +++ b/public/svgs/anythingllm.svg @@ -0,0 +1,166 @@ + + + + + + + diff --git a/public/svgs/argilla.png b/public/svgs/argilla.png new file mode 100644 index 000000000..3ead32785 Binary files /dev/null and b/public/svgs/argilla.png differ diff --git a/public/svgs/audiobookshelf.svg b/public/svgs/audiobookshelf.svg new file mode 100644 index 000000000..d641b765b --- /dev/null +++ b/public/svgs/audiobookshelf.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/svgs/azimutt.png b/public/svgs/azimutt.png new file mode 100644 index 000000000..ef69062cd Binary files /dev/null and b/public/svgs/azimutt.png differ diff --git a/public/svgs/bitcoin.svg b/public/svgs/bitcoin.svg new file mode 100644 index 000000000..2b75c99bc --- /dev/null +++ b/public/svgs/bitcoin.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/public/svgs/bookstack.png b/public/svgs/bookstack.png new file mode 100644 index 000000000..d10b3ca43 Binary files /dev/null and b/public/svgs/bookstack.png differ diff --git a/public/svgs/browserless.svg b/public/svgs/browserless.svg new file mode 100644 index 000000000..1d2d09a23 --- /dev/null +++ b/public/svgs/browserless.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/budge.png b/public/svgs/budge.png new file mode 100644 index 000000000..b44606c64 Binary files /dev/null and b/public/svgs/budge.png differ diff --git a/public/svgs/budibase.svg b/public/svgs/budibase.svg new file mode 100644 index 000000000..c77636bfe --- /dev/null +++ b/public/svgs/budibase.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/svgs/calcom.svg b/public/svgs/calcom.svg new file mode 100644 index 000000000..446b16655 --- /dev/null +++ b/public/svgs/calcom.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/public/svgs/castopod.svg b/public/svgs/castopod.svg new file mode 100644 index 000000000..c73008400 --- /dev/null +++ b/public/svgs/castopod.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/public/svgs/chaskiq.png b/public/svgs/chaskiq.png new file mode 100644 index 000000000..8f9f142ab Binary files /dev/null and b/public/svgs/chaskiq.png differ diff --git a/public/svgs/clickhouse.svg b/public/svgs/clickhouse.svg new file mode 100644 index 000000000..d536536de --- /dev/null +++ b/public/svgs/clickhouse.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/cloudbeaver.svg b/public/svgs/cloudbeaver.svg new file mode 100644 index 000000000..4a7634766 --- /dev/null +++ b/public/svgs/cloudbeaver.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/svgs/coder.svg b/public/svgs/coder.svg new file mode 100644 index 000000000..45b7f795c --- /dev/null +++ b/public/svgs/coder.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/svgs/coolify.png b/public/svgs/coolify.png new file mode 100644 index 000000000..fa01fec05 Binary files /dev/null and b/public/svgs/coolify.png differ diff --git a/public/svgs/cryptgeon.png b/public/svgs/cryptgeon.png new file mode 100644 index 000000000..be121cfd0 Binary files /dev/null and b/public/svgs/cryptgeon.png differ diff --git a/public/svgs/dify.png b/public/svgs/dify.png new file mode 100644 index 000000000..326acf789 Binary files /dev/null and b/public/svgs/dify.png differ diff --git a/public/svgs/docmost.png b/public/svgs/docmost.png new file mode 100644 index 000000000..ed049c7e6 Binary files /dev/null and b/public/svgs/docmost.png differ diff --git a/public/svgs/dozzle.svg b/public/svgs/dozzle.svg new file mode 100644 index 000000000..bf88e8729 --- /dev/null +++ b/public/svgs/dozzle.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/svgs/drupal.svg b/public/svgs/drupal.svg new file mode 100644 index 000000000..bd77106dc --- /dev/null +++ b/public/svgs/drupal.svg @@ -0,0 +1 @@ +Risorsa 28 \ No newline at end of file diff --git a/public/svgs/easyappointments.png b/public/svgs/easyappointments.png new file mode 100644 index 000000000..8f00d3353 Binary files /dev/null and b/public/svgs/easyappointments.png differ diff --git a/public/svgs/edgedb.svg b/public/svgs/edgedb.svg new file mode 100644 index 000000000..a906f7f7e --- /dev/null +++ b/public/svgs/edgedb.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/svgs/flowise.png b/public/svgs/flowise.png new file mode 100644 index 000000000..6b0be0d2a Binary files /dev/null and b/public/svgs/flowise.png differ diff --git a/public/svgs/forgejo.svg b/public/svgs/forgejo.svg new file mode 100644 index 000000000..804b05e28 --- /dev/null +++ b/public/svgs/forgejo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/foundryvtt.png b/public/svgs/foundryvtt.png new file mode 100644 index 000000000..c6a04508f Binary files /dev/null and b/public/svgs/foundryvtt.png differ diff --git a/public/svgs/freshrss.png b/public/svgs/freshrss.png new file mode 100644 index 000000000..d1a75118f Binary files /dev/null and b/public/svgs/freshrss.png differ diff --git a/public/svgs/getoutline.jpeg b/public/svgs/getoutline.jpeg new file mode 100644 index 000000000..422e402f7 Binary files /dev/null and b/public/svgs/getoutline.jpeg differ diff --git a/public/svgs/gitlab.svg b/public/svgs/gitlab.svg new file mode 100644 index 000000000..1c7cb0719 --- /dev/null +++ b/public/svgs/gitlab.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/glances.png b/public/svgs/glances.png new file mode 100644 index 000000000..31e1a09fb Binary files /dev/null and b/public/svgs/glances.png differ diff --git a/public/svgs/heyform.svg b/public/svgs/heyform.svg new file mode 100644 index 000000000..ff29ca654 --- /dev/null +++ b/public/svgs/heyform.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/svgs/homarr.svg b/public/svgs/homarr.svg new file mode 100644 index 000000000..4d4289604 --- /dev/null +++ b/public/svgs/homarr.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/svgs/immich.svg b/public/svgs/immich.svg new file mode 100644 index 000000000..9d844a772 --- /dev/null +++ b/public/svgs/immich.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/svgs/infisical.png b/public/svgs/infisical.png new file mode 100644 index 000000000..48eddae78 Binary files /dev/null and b/public/svgs/infisical.png differ diff --git a/public/svgs/it-tools.svg b/public/svgs/it-tools.svg new file mode 100644 index 000000000..3de5ecff7 --- /dev/null +++ b/public/svgs/it-tools.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/svgs/jenkins.svg b/public/svgs/jenkins.svg new file mode 100644 index 000000000..0529fff1e --- /dev/null +++ b/public/svgs/jenkins.svg @@ -0,0 +1,283 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/public/svgs/jitsi.svg b/public/svgs/jitsi.svg new file mode 100644 index 000000000..6257659ee --- /dev/null +++ b/public/svgs/jitsi.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/public/svgs/joplin.png b/public/svgs/joplin.png new file mode 100644 index 000000000..d17a1d2c1 Binary files /dev/null and b/public/svgs/joplin.png differ diff --git a/public/svgs/keycloak.svg b/public/svgs/keycloak.svg new file mode 100644 index 000000000..849ac2759 --- /dev/null +++ b/public/svgs/keycloak.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/svgs/kimai.svg b/public/svgs/kimai.svg new file mode 100644 index 000000000..35b146972 --- /dev/null +++ b/public/svgs/kimai.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/svgs/labelstudio.png b/public/svgs/labelstudio.png new file mode 100644 index 000000000..afa5160b9 Binary files /dev/null and b/public/svgs/labelstudio.png differ diff --git a/public/svgs/langfuse.png b/public/svgs/langfuse.png new file mode 100644 index 000000000..8dec0fe4a Binary files /dev/null and b/public/svgs/langfuse.png differ diff --git a/public/svgs/libreoffice.svg b/public/svgs/libreoffice.svg new file mode 100644 index 000000000..227471bef --- /dev/null +++ b/public/svgs/libreoffice.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/libretranslate.svg b/public/svgs/libretranslate.svg new file mode 100644 index 000000000..103d47d60 --- /dev/null +++ b/public/svgs/libretranslate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/litellm.svg b/public/svgs/litellm.svg new file mode 100644 index 000000000..01830c3f6 --- /dev/null +++ b/public/svgs/litellm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/litequeen.svg b/public/svgs/litequeen.svg new file mode 100644 index 000000000..aa0b8e038 --- /dev/null +++ b/public/svgs/litequeen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/mailpit.svg b/public/svgs/mailpit.svg new file mode 100644 index 000000000..e569e71cc --- /dev/null +++ b/public/svgs/mailpit.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/svgs/martin.png b/public/svgs/martin.png new file mode 100644 index 000000000..d1a99e148 Binary files /dev/null and b/public/svgs/martin.png differ diff --git a/public/svgs/mattermost.svg b/public/svgs/mattermost.svg new file mode 100644 index 000000000..b01d38eb7 --- /dev/null +++ b/public/svgs/mattermost.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/public/svgs/mautic.svg b/public/svgs/mautic.svg new file mode 100644 index 000000000..b528f72ef --- /dev/null +++ b/public/svgs/mautic.svg @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/public/svgs/mindsdb.svg b/public/svgs/mindsdb.svg new file mode 100644 index 000000000..53799dd1c --- /dev/null +++ b/public/svgs/mindsdb.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/svgs/minecraft.svg b/public/svgs/minecraft.svg new file mode 100644 index 000000000..10ae9042c --- /dev/null +++ b/public/svgs/minecraft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/mixpost.svg b/public/svgs/mixpost.svg new file mode 100644 index 000000000..bd915e77a --- /dev/null +++ b/public/svgs/mixpost.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/svgs/mosquitto.png b/public/svgs/mosquitto.png new file mode 100644 index 000000000..eb287a7cd Binary files /dev/null and b/public/svgs/mosquitto.png differ diff --git a/public/svgs/nitropage.svg b/public/svgs/nitropage.svg new file mode 100644 index 000000000..67b2df17f --- /dev/null +++ b/public/svgs/nitropage.svg @@ -0,0 +1,8 @@ + + + NP + + + + + diff --git a/public/svgs/ntfy.svg b/public/svgs/ntfy.svg new file mode 100644 index 000000000..9e5b5136f --- /dev/null +++ b/public/svgs/ntfy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/ollama.svg b/public/svgs/ollama.svg new file mode 100644 index 000000000..3df9a9fba --- /dev/null +++ b/public/svgs/ollama.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/svgs/onedev.svg b/public/svgs/onedev.svg new file mode 100644 index 000000000..fb9c9c060 --- /dev/null +++ b/public/svgs/onedev.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/svgs/organizr.png b/public/svgs/organizr.png new file mode 100644 index 000000000..92541ea72 Binary files /dev/null and b/public/svgs/organizr.png differ diff --git a/public/svgs/osticket.png b/public/svgs/osticket.png new file mode 100644 index 000000000..65885b71b Binary files /dev/null and b/public/svgs/osticket.png differ diff --git a/public/svgs/owncloud.svg b/public/svgs/owncloud.svg new file mode 100644 index 000000000..83631e3f5 --- /dev/null +++ b/public/svgs/owncloud.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/svgs/paperless.svg b/public/svgs/paperless.svg new file mode 100644 index 000000000..347b1e759 --- /dev/null +++ b/public/svgs/paperless.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/public/svgs/peppermint.png b/public/svgs/peppermint.png new file mode 100644 index 000000000..38db83de0 Binary files /dev/null and b/public/svgs/peppermint.png differ diff --git a/public/svgs/plane.svg b/public/svgs/plane.svg new file mode 100644 index 000000000..899e1a866 --- /dev/null +++ b/public/svgs/plane.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/public/svgs/plunk.svg b/public/svgs/plunk.svg new file mode 100644 index 000000000..3f6ed4792 --- /dev/null +++ b/public/svgs/plunk.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/postgresql.svg b/public/svgs/postgresql.svg new file mode 100644 index 000000000..1ff223856 --- /dev/null +++ b/public/svgs/postgresql.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/prefect.png b/public/svgs/prefect.png new file mode 100644 index 000000000..2f87ec0d7 Binary files /dev/null and b/public/svgs/prefect.png differ diff --git a/public/svgs/qbittorrent.svg b/public/svgs/qbittorrent.svg new file mode 100644 index 000000000..69d8cf62a --- /dev/null +++ b/public/svgs/qbittorrent.svg @@ -0,0 +1,16 @@ + + + qbittorrent-new-light + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/svgs/qdrant.png b/public/svgs/qdrant.png new file mode 100644 index 000000000..ecb2a56d5 Binary files /dev/null and b/public/svgs/qdrant.png differ diff --git a/public/svgs/rabbitmq.svg b/public/svgs/rabbitmq.svg new file mode 100644 index 000000000..7a94d71bb --- /dev/null +++ b/public/svgs/rabbitmq.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/searxng.svg b/public/svgs/searxng.svg new file mode 100644 index 000000000..b94fe3728 --- /dev/null +++ b/public/svgs/searxng.svg @@ -0,0 +1,56 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/public/svgs/soketi.jpeg b/public/svgs/soketi.jpeg new file mode 100644 index 000000000..00c8d82cd Binary files /dev/null and b/public/svgs/soketi.jpeg differ diff --git a/public/svgs/statusnook.svg b/public/svgs/statusnook.svg new file mode 100644 index 000000000..74fd69651 --- /dev/null +++ b/public/svgs/statusnook.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/svgs/strapi.svg b/public/svgs/strapi.svg new file mode 100644 index 000000000..d1a71abc2 --- /dev/null +++ b/public/svgs/strapi.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/public/svgs/supertokens.svg b/public/svgs/supertokens.svg new file mode 100644 index 000000000..30b435bd0 --- /dev/null +++ b/public/svgs/supertokens.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/svgs/tolgee.svg b/public/svgs/tolgee.svg new file mode 100644 index 000000000..0f216e0c6 --- /dev/null +++ b/public/svgs/tolgee.svg @@ -0,0 +1,5 @@ + + + + diff --git a/public/svgs/traccar.png b/public/svgs/traccar.png new file mode 100644 index 000000000..c747aea05 Binary files /dev/null and b/public/svgs/traccar.png differ diff --git a/public/svgs/transmission.svg b/public/svgs/transmission.svg new file mode 100644 index 000000000..9a11f77f4 --- /dev/null +++ b/public/svgs/transmission.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/unsend.svg b/public/svgs/unsend.svg new file mode 100644 index 000000000..f5ff6fabc --- /dev/null +++ b/public/svgs/unsend.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/svgs/unstructured.png b/public/svgs/unstructured.png new file mode 100644 index 000000000..a6ec855b6 Binary files /dev/null and b/public/svgs/unstructured.png differ diff --git a/public/svgs/vvveb.svg b/public/svgs/vvveb.svg new file mode 100644 index 000000000..2b66b3087 --- /dev/null +++ b/public/svgs/vvveb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/weaviate.png b/public/svgs/weaviate.png new file mode 100644 index 000000000..134294253 Binary files /dev/null and b/public/svgs/weaviate.png differ diff --git a/public/svgs/windmill.svg b/public/svgs/windmill.svg new file mode 100644 index 000000000..2b06716f9 --- /dev/null +++ b/public/svgs/windmill.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + diff --git a/public/svgs/wireguard.svg b/public/svgs/wireguard.svg new file mode 100644 index 000000000..81823b3eb --- /dev/null +++ b/public/svgs/wireguard.svg @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/public/svgs/zep.png b/public/svgs/zep.png new file mode 100644 index 000000000..7d51b32dc Binary files /dev/null and b/public/svgs/zep.png differ diff --git a/public/svgs/zipline.png b/public/svgs/zipline.png new file mode 100644 index 000000000..2b8f6972d Binary files /dev/null and b/public/svgs/zipline.png differ diff --git a/public/vendor/telescope/app-dark.css b/public/vendor/telescope/app-dark.css new file mode 100644 index 000000000..9559860e7 --- /dev/null +++ b/public/vendor/telescope/app-dark.css @@ -0,0 +1,8 @@ +@charset "UTF-8";.form-control:-moz-focusring{text-shadow:none!important} + +/*! + * Bootstrap v4.6.2 (https://getbootstrap.com/) + * Copyright 2011-2022 The Bootstrap Authors + * Copyright 2011-2022 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#4b5563;--gray-dark:#1f2937;--primary:#4040c8;--secondary:#4b5563;--success:#059669;--info:#2563eb;--warning:#d97706;--danger:#dc2626;--light:#f3f4f6;--dark:#1f2937;--breakpoint-xs:0;--breakpoint-sm:2px;--breakpoint-md:8px;--breakpoint-lg:9px;--breakpoint-xl:10px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,:after,:before{box-sizing:border-box}html{-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);font-family:sans-serif;line-height:1.15}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{background-color:#111827;color:#f3f4f6;font-family:Figtree,sans-serif;font-size:1rem;font-weight:400;line-height:1.5;margin:0;text-align:left}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;margin-top:0}p{margin-bottom:1rem;margin-top:0}abbr[data-original-title],abbr[title]{border-bottom:0;cursor:help;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{font-style:normal;line-height:inherit}address,dl,ol,ul{margin-bottom:1rem}dl,ol,ul{margin-top:0}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:600}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{background-color:transparent;color:#818cf8;text-decoration:none}a:hover{color:#a5b4fc;text-decoration:underline}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}pre{-ms-overflow-style:scrollbar;margin-bottom:1rem;margin-top:0;overflow:auto}figure{margin:0 0 1rem}img{border-style:none}img,svg{vertical-align:middle}svg{overflow:hidden}table{border-collapse:collapse}caption{caption-side:bottom;color:#9ca3af;padding-bottom:.75rem;padding-top:.75rem;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit;margin:0}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{border:0;margin:0;min-width:0;padding:0}legend{color:inherit;display:block;font-size:1.5rem;line-height:inherit;margin-bottom:.5rem;max-width:100%;padding:0;white-space:normal;width:100%}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:none;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}output{display:inline-block}summary{cursor:pointer;display:list-item}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-weight:500;line-height:1.2;margin-bottom:.5rem}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem}.display-1,.display-2{font-weight:300;line-height:1.2}.display-2{font-size:5.5rem}.display-3{font-size:4.5rem}.display-3,.display-4{font-weight:300;line-height:1.2}.display-4{font-size:3.5rem}hr{border:0;border-top:1px solid rgba(0,0,0,.1);margin-bottom:1rem;margin-top:1rem}.small,small{font-size:.875em;font-weight:400}.mark,mark{background-color:#fcf8e3;padding:.2em}.list-inline,.list-unstyled{list-style:none;padding-left:0}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{font-size:1.25rem;margin-bottom:1rem}.blockquote-footer{color:#4b5563;display:block;font-size:.875em}.blockquote-footer:before{content:"— "}.img-fluid,.img-thumbnail{height:auto;max-width:100%}.img-thumbnail{background-color:#111827;border:1px solid #d1d5db;border-radius:.25rem;padding:.25rem}.figure{display:inline-block}.figure-img{line-height:1;margin-bottom:.5rem}.figure-caption{color:#4b5563;font-size:90%}code{word-wrap:break-word;color:#e83e8c;font-size:87.5%}a>code{color:inherit}kbd{background-color:#111827;border-radius:.2rem;color:#fff;font-size:87.5%;padding:.2rem .4rem}kbd kbd{font-size:100%;font-weight:600;padding:0}pre{color:#111827;display:block;font-size:87.5%}pre code{color:inherit;font-size:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{margin-left:auto;margin-right:auto;padding-left:15px;padding-right:15px;width:100%}@media (min-width:2px){.container,.container-sm{max-width:1137px}}@media (min-width:8px){.container,.container-md,.container-sm{max-width:1138px}}@media (min-width:9px){.container,.container-lg,.container-md,.container-sm{max-width:1139px}}@media (min-width:10px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:flex;flex-wrap:wrap;margin-left:-15px;margin-right:-15px}.no-gutters{margin-left:0;margin-right:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-left:0;padding-right:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{padding-left:15px;padding-right:15px;position:relative;width:100%}.col{flex-basis:0;flex-grow:1;max-width:100%}.row-cols-1>*{flex:0 0 100%;max-width:100%}.row-cols-2>*{flex:0 0 50%;max-width:50%}.row-cols-3>*{flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-4>*{flex:0 0 25%;max-width:25%}.row-cols-5>*{flex:0 0 20%;max-width:20%}.row-cols-6>*{flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-auto{flex:0 0 auto;max-width:100%;width:auto}.col-1{flex:0 0 8.33333333%;max-width:8.33333333%}.col-2{flex:0 0 16.66666667%;max-width:16.66666667%}.col-3{flex:0 0 25%;max-width:25%}.col-4{flex:0 0 33.33333333%;max-width:33.33333333%}.col-5{flex:0 0 41.66666667%;max-width:41.66666667%}.col-6{flex:0 0 50%;max-width:50%}.col-7{flex:0 0 58.33333333%;max-width:58.33333333%}.col-8{flex:0 0 66.66666667%;max-width:66.66666667%}.col-9{flex:0 0 75%;max-width:75%}.col-10{flex:0 0 83.33333333%;max-width:83.33333333%}.col-11{flex:0 0 91.66666667%;max-width:91.66666667%}.col-12{flex:0 0 100%;max-width:100%}.order-first{order:-1}.order-last{order:13}.order-0{order:0}.order-1{order:1}.order-2{order:2}.order-3{order:3}.order-4{order:4}.order-5{order:5}.order-6{order:6}.order-7{order:7}.order-8{order:8}.order-9{order:9}.order-10{order:10}.order-11{order:11}.order-12{order:12}.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%}@media (min-width:2px){.col-sm{flex-basis:0;flex-grow:1;max-width:100%}.row-cols-sm-1>*{flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-sm-auto{flex:0 0 auto;max-width:100%;width:auto}.col-sm-1{flex:0 0 8.33333333%;max-width:8.33333333%}.col-sm-2{flex:0 0 16.66666667%;max-width:16.66666667%}.col-sm-3{flex:0 0 25%;max-width:25%}.col-sm-4{flex:0 0 33.33333333%;max-width:33.33333333%}.col-sm-5{flex:0 0 41.66666667%;max-width:41.66666667%}.col-sm-6{flex:0 0 50%;max-width:50%}.col-sm-7{flex:0 0 58.33333333%;max-width:58.33333333%}.col-sm-8{flex:0 0 66.66666667%;max-width:66.66666667%}.col-sm-9{flex:0 0 75%;max-width:75%}.col-sm-10{flex:0 0 83.33333333%;max-width:83.33333333%}.col-sm-11{flex:0 0 91.66666667%;max-width:91.66666667%}.col-sm-12{flex:0 0 100%;max-width:100%}.order-sm-first{order:-1}.order-sm-last{order:13}.order-sm-0{order:0}.order-sm-1{order:1}.order-sm-2{order:2}.order-sm-3{order:3}.order-sm-4{order:4}.order-sm-5{order:5}.order-sm-6{order:6}.order-sm-7{order:7}.order-sm-8{order:8}.order-sm-9{order:9}.order-sm-10{order:10}.order-sm-11{order:11}.order-sm-12{order:12}.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%}}@media (min-width:8px){.col-md{flex-basis:0;flex-grow:1;max-width:100%}.row-cols-md-1>*{flex:0 0 100%;max-width:100%}.row-cols-md-2>*{flex:0 0 50%;max-width:50%}.row-cols-md-3>*{flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-md-4>*{flex:0 0 25%;max-width:25%}.row-cols-md-5>*{flex:0 0 20%;max-width:20%}.row-cols-md-6>*{flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-md-auto{flex:0 0 auto;max-width:100%;width:auto}.col-md-1{flex:0 0 8.33333333%;max-width:8.33333333%}.col-md-2{flex:0 0 16.66666667%;max-width:16.66666667%}.col-md-3{flex:0 0 25%;max-width:25%}.col-md-4{flex:0 0 33.33333333%;max-width:33.33333333%}.col-md-5{flex:0 0 41.66666667%;max-width:41.66666667%}.col-md-6{flex:0 0 50%;max-width:50%}.col-md-7{flex:0 0 58.33333333%;max-width:58.33333333%}.col-md-8{flex:0 0 66.66666667%;max-width:66.66666667%}.col-md-9{flex:0 0 75%;max-width:75%}.col-md-10{flex:0 0 83.33333333%;max-width:83.33333333%}.col-md-11{flex:0 0 91.66666667%;max-width:91.66666667%}.col-md-12{flex:0 0 100%;max-width:100%}.order-md-first{order:-1}.order-md-last{order:13}.order-md-0{order:0}.order-md-1{order:1}.order-md-2{order:2}.order-md-3{order:3}.order-md-4{order:4}.order-md-5{order:5}.order-md-6{order:6}.order-md-7{order:7}.order-md-8{order:8}.order-md-9{order:9}.order-md-10{order:10}.order-md-11{order:11}.order-md-12{order:12}.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%}}@media (min-width:9px){.col-lg{flex-basis:0;flex-grow:1;max-width:100%}.row-cols-lg-1>*{flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-lg-auto{flex:0 0 auto;max-width:100%;width:auto}.col-lg-1{flex:0 0 8.33333333%;max-width:8.33333333%}.col-lg-2{flex:0 0 16.66666667%;max-width:16.66666667%}.col-lg-3{flex:0 0 25%;max-width:25%}.col-lg-4{flex:0 0 33.33333333%;max-width:33.33333333%}.col-lg-5{flex:0 0 41.66666667%;max-width:41.66666667%}.col-lg-6{flex:0 0 50%;max-width:50%}.col-lg-7{flex:0 0 58.33333333%;max-width:58.33333333%}.col-lg-8{flex:0 0 66.66666667%;max-width:66.66666667%}.col-lg-9{flex:0 0 75%;max-width:75%}.col-lg-10{flex:0 0 83.33333333%;max-width:83.33333333%}.col-lg-11{flex:0 0 91.66666667%;max-width:91.66666667%}.col-lg-12{flex:0 0 100%;max-width:100%}.order-lg-first{order:-1}.order-lg-last{order:13}.order-lg-0{order:0}.order-lg-1{order:1}.order-lg-2{order:2}.order-lg-3{order:3}.order-lg-4{order:4}.order-lg-5{order:5}.order-lg-6{order:6}.order-lg-7{order:7}.order-lg-8{order:8}.order-lg-9{order:9}.order-lg-10{order:10}.order-lg-11{order:11}.order-lg-12{order:12}.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%}}@media (min-width:10px){.col-xl{flex-basis:0;flex-grow:1;max-width:100%}.row-cols-xl-1>*{flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-xl-auto{flex:0 0 auto;max-width:100%;width:auto}.col-xl-1{flex:0 0 8.33333333%;max-width:8.33333333%}.col-xl-2{flex:0 0 16.66666667%;max-width:16.66666667%}.col-xl-3{flex:0 0 25%;max-width:25%}.col-xl-4{flex:0 0 33.33333333%;max-width:33.33333333%}.col-xl-5{flex:0 0 41.66666667%;max-width:41.66666667%}.col-xl-6{flex:0 0 50%;max-width:50%}.col-xl-7{flex:0 0 58.33333333%;max-width:58.33333333%}.col-xl-8{flex:0 0 66.66666667%;max-width:66.66666667%}.col-xl-9{flex:0 0 75%;max-width:75%}.col-xl-10{flex:0 0 83.33333333%;max-width:83.33333333%}.col-xl-11{flex:0 0 91.66666667%;max-width:91.66666667%}.col-xl-12{flex:0 0 100%;max-width:100%}.order-xl-first{order:-1}.order-xl-last{order:13}.order-xl-0{order:0}.order-xl-1{order:1}.order-xl-2{order:2}.order-xl-3{order:3}.order-xl-4{order:4}.order-xl-5{order:5}.order-xl-6{order:6}.order-xl-7{order:7}.order-xl-8{order:8}.order-xl-9{order:9}.order-xl-10{order:10}.order-xl-11{order:11}.order-xl-12{order:12}.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%}}.table{color:#f3f4f6;margin-bottom:1rem;width:100%}.table td,.table th{border-top:1px solid #374151;padding:.75rem;vertical-align:top}.table thead th{border-bottom:2px solid #374151;vertical-align:bottom}.table tbody+tbody{border-top:2px solid #374151}.table-sm td,.table-sm th{padding:.3rem}.table-bordered,.table-bordered td,.table-bordered th{border:1px solid #374151}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:#374151;color:#f3f4f6}.table-primary,.table-primary>td,.table-primary>th{background-color:#cacaf0}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#9c9ce2}.table-hover .table-primary:hover,.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#b6b6ea}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#cdcfd3}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#a1a7ae}.table-hover .table-secondary:hover,.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#bfc2c7}.table-success,.table-success>td,.table-success>th{background-color:#b9e2d5}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#7dc8b1}.table-hover .table-success:hover,.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#a7dbca}.table-info,.table-info>td,.table-info>th{background-color:#c2d3f9}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#8eaef5}.table-hover .table-info:hover,.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abc2f7}.table-warning,.table-warning>td,.table-warning>th{background-color:#f4d9b9}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ebb87e}.table-hover .table-warning:hover,.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#f1cda3}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c2c2}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed8e8e}.table-hover .table-danger:hover,.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1acac}.table-light,.table-light>td,.table-light>th{background-color:#fcfcfc}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#f9f9fa}.table-hover .table-light:hover,.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#efefef}.table-dark,.table-dark>td,.table-dark>th{background-color:#c0c3c7}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#8b9097}.table-hover .table-dark:hover,.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b3b6bb}.table-active,.table-active>td,.table-active>th{background-color:#374151}.table-hover .table-active:hover,.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:#2d3542}.table .thead-dark th{background-color:#1f2937;border-color:#2d3b4f;color:#fff}.table .thead-light th{background-color:#e5e7eb;border-color:#374151;color:#374151}.table-dark{background-color:#1f2937;color:#fff}.table-dark td,.table-dark th,.table-dark thead th{border-color:#2d3b4f}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:hsla(0,0%,100%,.05)}.table-dark.table-hover tbody tr:hover{background-color:hsla(0,0%,100%,.075);color:#fff}@media (max-width:1.98px){.table-responsive-sm{-webkit-overflow-scrolling:touch;display:block;overflow-x:auto;width:100%}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:7.98px){.table-responsive-md{-webkit-overflow-scrolling:touch;display:block;overflow-x:auto;width:100%}.table-responsive-md>.table-bordered{border:0}}@media (max-width:8.98px){.table-responsive-lg{-webkit-overflow-scrolling:touch;display:block;overflow-x:auto;width:100%}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:9.98px){.table-responsive-xl{-webkit-overflow-scrolling:touch;display:block;overflow-x:auto;width:100%}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{-webkit-overflow-scrolling:touch;display:block;overflow-x:auto;width:100%}.table-responsive>.table-bordered{border:0}.form-control{background-clip:padding-box;background-color:#1f2937;border:1px solid #4b5563;border-radius:.25rem;color:#e5e7eb;display:block;font-size:1rem;font-weight:400;height:calc(1.5em + .75rem + 2px);line-height:1.5;padding:.375rem .75rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;width:100%}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{background-color:#1f2937;border-color:#a3a3e5;box-shadow:0 0 0 .2rem rgba(64,64,200,.25);color:#e5e7eb;outline:0}.form-control::-moz-placeholder{color:#4b5563;opacity:1}.form-control::placeholder{color:#4b5563;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e5e7eb;opacity:1}input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{-webkit-appearance:none;-moz-appearance:none;appearance:none}select.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #e5e7eb}select.form-control:focus::-ms-value{background-color:#1f2937;color:#e5e7eb}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{font-size:inherit;line-height:1.5;margin-bottom:0;padding-bottom:calc(.375rem + 1px);padding-top:calc(.375rem + 1px)}.col-form-label-lg{font-size:1.25rem;line-height:1.5;padding-bottom:calc(.5rem + 1px);padding-top:calc(.5rem + 1px)}.col-form-label-sm{font-size:.875rem;line-height:1.5;padding-bottom:calc(.25rem + 1px);padding-top:calc(.25rem + 1px)}.form-control-plaintext{background-color:transparent;border:solid transparent;border-width:1px 0;color:#f3f4f6;display:block;font-size:1rem;line-height:1.5;margin-bottom:0;padding:.375rem 0;width:100%}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-left:0;padding-right:0}.form-control-sm{border-radius:.2rem;font-size:.875rem;height:calc(1.5em + .5rem + 2px);line-height:1.5;padding:.25rem .5rem}.form-control-lg{border-radius:6px;font-size:1.25rem;height:calc(1.5em + 1rem + 2px);line-height:1.5;padding:.5rem 1rem}select.form-control[multiple],select.form-control[size],textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:flex;flex-wrap:wrap;margin-left:-5px;margin-right:-5px}.form-row>.col,.form-row>[class*=col-]{padding-left:5px;padding-right:5px}.form-check{display:block;padding-left:1.25rem;position:relative}.form-check-input{margin-left:-1.25rem;margin-top:.3rem;position:absolute}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{color:#9ca3af}.form-check-label{margin-bottom:0}.form-check-inline{align-items:center;display:inline-flex;margin-right:.75rem;padding-left:0}.form-check-inline .form-check-input{margin-left:0;margin-right:.3125rem;margin-top:0;position:static}.valid-feedback{color:#059669;display:none;font-size:.875em;margin-top:.25rem;width:100%}.valid-tooltip{background-color:rgba(5,150,105,.9);border-radius:.25rem;color:#fff;display:none;font-size:.875rem;left:0;line-height:1.5;margin-top:.1rem;max-width:100%;padding:.25rem .5rem;position:absolute;top:100%;z-index:5}.form-row>.col>.valid-tooltip,.form-row>[class*=col-]>.valid-tooltip{left:5px}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath fill='%23059669' 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.1z'/%3E%3C/svg%3E");background-position:right calc(.375em + .1875rem) center;background-repeat:no-repeat;background-size:calc(.75em + .375rem) calc(.75em + .375rem);border-color:#059669;padding-right:calc(1.5em + .75rem)!important}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#059669;box-shadow:0 0 0 .2rem rgba(5,150,105,.25)}.was-validated select.form-control:valid,select.form-control.is-valid{background-position:right 1.5rem center;padding-right:3rem!important}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem);padding-right:calc(1.5em + .75rem)}.custom-select.is-valid,.was-validated .custom-select:valid{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5'%3E%3Cpath fill='%231f2937' d='M2 0 0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") right .75rem center/8px 10px no-repeat,#1f2937 url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath fill='%23059669' 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.1z'/%3E%3C/svg%3E") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat;border-color:#059669;padding-right:calc(.75em + 2.3125rem)!important}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#059669;box-shadow:0 0 0 .2rem rgba(5,150,105,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#059669}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#059669}.custom-control-input.is-valid~.custom-control-label:before,.was-validated .custom-control-input:valid~.custom-control-label:before{border-color:#059669}.custom-control-input.is-valid:checked~.custom-control-label:before,.was-validated .custom-control-input:valid:checked~.custom-control-label:before{background-color:#07c78c;border-color:#07c78c}.custom-control-input.is-valid:focus~.custom-control-label:before,.was-validated .custom-control-input:valid:focus~.custom-control-label:before{box-shadow:0 0 0 .2rem rgba(5,150,105,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label:before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label:before{border-color:#059669}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#059669}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#059669;box-shadow:0 0 0 .2rem rgba(5,150,105,.25)}.invalid-feedback{color:#dc2626;display:none;font-size:.875em;margin-top:.25rem;width:100%}.invalid-tooltip{background-color:rgba(220,38,38,.9);border-radius:.25rem;color:#fff;display:none;font-size:.875rem;left:0;line-height:1.5;margin-top:.1rem;max-width:100%;padding:.25rem .5rem;position:absolute;top:100%;z-index:5}.form-row>.col>.invalid-tooltip,.form-row>[class*=col-]>.invalid-tooltip{left:5px}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc2626'%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='%23dc2626' stroke='none'/%3E%3C/svg%3E");background-position:right calc(.375em + .1875rem) center;background-repeat:no-repeat;background-size:calc(.75em + .375rem) calc(.75em + .375rem);border-color:#dc2626;padding-right:calc(1.5em + .75rem)!important}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc2626;box-shadow:0 0 0 .2rem rgba(220,38,38,.25)}.was-validated select.form-control:invalid,select.form-control.is-invalid{background-position:right 1.5rem center;padding-right:3rem!important}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem);padding-right:calc(1.5em + .75rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5'%3E%3Cpath fill='%231f2937' d='M2 0 0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") right .75rem center/8px 10px no-repeat,#1f2937 url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc2626'%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='%23dc2626' stroke='none'/%3E%3C/svg%3E") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat;border-color:#dc2626;padding-right:calc(.75em + 2.3125rem)!important}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc2626;box-shadow:0 0 0 .2rem rgba(220,38,38,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc2626}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc2626}.custom-control-input.is-invalid~.custom-control-label:before,.was-validated .custom-control-input:invalid~.custom-control-label:before{border-color:#dc2626}.custom-control-input.is-invalid:checked~.custom-control-label:before,.was-validated .custom-control-input:invalid:checked~.custom-control-label:before{background-color:#e35252;border-color:#e35252}.custom-control-input.is-invalid:focus~.custom-control-label:before,.was-validated .custom-control-input:invalid:focus~.custom-control-label:before{box-shadow:0 0 0 .2rem rgba(220,38,38,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label:before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label:before{border-color:#dc2626}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc2626}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc2626;box-shadow:0 0 0 .2rem rgba(220,38,38,.25)}.form-inline{align-items:center;display:flex;flex-flow:row wrap}.form-inline .form-check{width:100%}@media (min-width:2px){.form-inline label{justify-content:center}.form-inline .form-group,.form-inline label{align-items:center;display:flex;margin-bottom:0}.form-inline .form-group{flex:0 0 auto;flex-flow:row wrap}.form-inline .form-control{display:inline-block;vertical-align:middle;width:auto}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{align-items:center;display:flex;justify-content:center;padding-left:0;width:auto}.form-inline .form-check-input{flex-shrink:0;margin-left:0;margin-right:.25rem;margin-top:0;position:relative}.form-inline .custom-control{align-items:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;color:#f3f4f6;display:inline-block;font-size:1rem;font-weight:400;line-height:1.5;padding:.375rem .75rem;text-align:center;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#f3f4f6;text-decoration:none}.btn.focus,.btn:focus{box-shadow:0 0 0 .2rem rgba(64,64,200,.25);outline:0}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{background-color:#4040c8;border-color:#4040c8;color:#fff}.btn-primary.focus,.btn-primary:focus,.btn-primary:hover{background-color:#3232af;border-color:#3030a5;color:#fff}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 .2rem rgba(93,93,208,.5)}.btn-primary.disabled,.btn-primary:disabled{background-color:#4040c8;border-color:#4040c8;color:#fff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{background-color:#3030a5;border-color:#2d2d9b;color:#fff}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(93,93,208,.5)}.btn-secondary{background-color:#4b5563;border-color:#4b5563;color:#fff}.btn-secondary.focus,.btn-secondary:focus,.btn-secondary:hover{background-color:#3b424d;border-color:#353c46;color:#fff}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 .2rem hsla(213,9%,44%,.5)}.btn-secondary.disabled,.btn-secondary:disabled{background-color:#4b5563;border-color:#4b5563;color:#fff}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{background-color:#353c46;border-color:#30363f;color:#fff}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem hsla(213,9%,44%,.5)}.btn-success{background-color:#059669;border-color:#059669;color:#fff}.btn-success.focus,.btn-success:focus,.btn-success:hover{background-color:#04714f;border-color:#036546;color:#fff}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 .2rem rgba(43,166,128,.5)}.btn-success.disabled,.btn-success:disabled{background-color:#059669;border-color:#059669;color:#fff}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{background-color:#036546;border-color:#03583e;color:#fff}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(43,166,128,.5)}.btn-info{background-color:#2563eb;border-color:#2563eb;color:#fff}.btn-info.focus,.btn-info:focus,.btn-info:hover{background-color:#1451d6;border-color:#134cca;color:#fff}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 .2rem rgba(70,122,238,.5)}.btn-info.disabled,.btn-info:disabled{background-color:#2563eb;border-color:#2563eb;color:#fff}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{background-color:#134cca;border-color:#1248bf;color:#fff}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(70,122,238,.5)}.btn-warning{background-color:#d97706;border-color:#d97706;color:#fff}.btn-warning.focus,.btn-warning:focus,.btn-warning:hover{background-color:#b46305;border-color:#a75c05;color:#fff}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 .2rem rgba(223,139,43,.5)}.btn-warning.disabled,.btn-warning:disabled{background-color:#d97706;border-color:#d97706;color:#fff}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{background-color:#a75c05;border-color:#9b5504;color:#fff}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(223,139,43,.5)}.btn-danger{background-color:#dc2626;border-color:#dc2626;color:#fff}.btn-danger.focus,.btn-danger:focus,.btn-danger:hover{background-color:#bd1f1f;border-color:#b21d1d;color:#fff}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 .2rem rgba(225,71,71,.5)}.btn-danger.disabled,.btn-danger:disabled{background-color:#dc2626;border-color:#dc2626;color:#fff}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{background-color:#b21d1d;border-color:#a71b1b;color:#fff}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,71,71,.5)}.btn-light{background-color:#f3f4f6;border-color:#f3f4f6;color:#111827}.btn-light.focus,.btn-light:focus,.btn-light:hover{background-color:#dde0e6;border-color:#d6d9e0;color:#111827}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 .2rem hsla(220,7%,83%,.5)}.btn-light.disabled,.btn-light:disabled{background-color:#f3f4f6;border-color:#f3f4f6;color:#111827}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{background-color:#d6d9e0;border-color:#cfd3db;color:#111827}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem hsla(220,7%,83%,.5)}.btn-dark{background-color:#1f2937;border-color:#1f2937;color:#fff}.btn-dark.focus,.btn-dark:focus,.btn-dark:hover{background-color:#11171f;border-color:#0d1116;color:#fff}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 .2rem rgba(65,73,85,.5)}.btn-dark.disabled,.btn-dark:disabled{background-color:#1f2937;border-color:#1f2937;color:#fff}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{background-color:#0d1116;border-color:#080b0e;color:#fff}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(65,73,85,.5)}.btn-outline-primary{border-color:#4040c8;color:#4040c8}.btn-outline-primary:hover{background-color:#4040c8;border-color:#4040c8;color:#fff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(64,64,200,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{background-color:transparent;color:#4040c8}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{background-color:#4040c8;border-color:#4040c8;color:#fff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(64,64,200,.5)}.btn-outline-secondary{border-color:#4b5563;color:#4b5563}.btn-outline-secondary:hover{background-color:#4b5563;border-color:#4b5563;color:#fff}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(75,85,99,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{background-color:transparent;color:#4b5563}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{background-color:#4b5563;border-color:#4b5563;color:#fff}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(75,85,99,.5)}.btn-outline-success{border-color:#059669;color:#059669}.btn-outline-success:hover{background-color:#059669;border-color:#059669;color:#fff}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(5,150,105,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{background-color:transparent;color:#059669}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{background-color:#059669;border-color:#059669;color:#fff}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(5,150,105,.5)}.btn-outline-info{border-color:#2563eb;color:#2563eb}.btn-outline-info:hover{background-color:#2563eb;border-color:#2563eb;color:#fff}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(37,99,235,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{background-color:transparent;color:#2563eb}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{background-color:#2563eb;border-color:#2563eb;color:#fff}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(37,99,235,.5)}.btn-outline-warning{border-color:#d97706;color:#d97706}.btn-outline-warning:hover{background-color:#d97706;border-color:#d97706;color:#fff}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(217,119,6,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{background-color:transparent;color:#d97706}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{background-color:#d97706;border-color:#d97706;color:#fff}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(217,119,6,.5)}.btn-outline-danger{border-color:#dc2626;color:#dc2626}.btn-outline-danger:hover{background-color:#dc2626;border-color:#dc2626;color:#fff}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,38,38,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{background-color:transparent;color:#dc2626}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{background-color:#dc2626;border-color:#dc2626;color:#fff}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,38,38,.5)}.btn-outline-light{border-color:#f3f4f6;color:#f3f4f6}.btn-outline-light:hover{background-color:#f3f4f6;border-color:#f3f4f6;color:#111827}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(243,244,246,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{background-color:transparent;color:#f3f4f6}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{background-color:#f3f4f6;border-color:#f3f4f6;color:#111827}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(243,244,246,.5)}.btn-outline-dark{border-color:#1f2937;color:#1f2937}.btn-outline-dark:hover{background-color:#1f2937;border-color:#1f2937;color:#fff}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(31,41,55,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{background-color:transparent;color:#1f2937}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{background-color:#1f2937;border-color:#1f2937;color:#fff}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(31,41,55,.5)}.btn-link{color:#818cf8;font-weight:400;text-decoration:none}.btn-link:hover{color:#a5b4fc}.btn-link.focus,.btn-link:focus,.btn-link:hover{text-decoration:underline}.btn-link.disabled,.btn-link:disabled{color:#4b5563;pointer-events:none}.btn-group-lg>.btn,.btn-lg{border-radius:6px;font-size:1.25rem;line-height:1.5;padding:.5rem 1rem}.btn-group-sm>.btn,.btn-sm{border-radius:.2rem;font-size:.875rem;line-height:1.5;padding:.25rem .5rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .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;position:relative;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.width{height:auto;transition:width .35s ease;width:0}@media (prefers-reduced-motion:reduce){.collapsing.width{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle:after{border-bottom:0;border-left:.3em solid transparent;border-right:.3em solid transparent;border-top:.3em solid;content:"";display:inline-block;margin-left:.255em;vertical-align:.255em}.dropdown-toggle:empty:after{margin-left:0}.dropdown-menu{background-clip:padding-box;background-color:#374151;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;color:#f3f4f6;display:none;float:left;font-size:1rem;left:0;list-style:none;margin:.125rem 0 0;min-width:10rem;padding:.5rem 0;position:absolute;text-align:left;top:100%;z-index:1000}.dropdown-menu-left{left:0;right:auto}.dropdown-menu-right{left:auto;right:0}@media (min-width:2px){.dropdown-menu-sm-left{left:0;right:auto}.dropdown-menu-sm-right{left:auto;right:0}}@media (min-width:8px){.dropdown-menu-md-left{left:0;right:auto}.dropdown-menu-md-right{left:auto;right:0}}@media (min-width:9px){.dropdown-menu-lg-left{left:0;right:auto}.dropdown-menu-lg-right{left:auto;right:0}}@media (min-width:10px){.dropdown-menu-xl-left{left:0;right:auto}.dropdown-menu-xl-right{left:auto;right:0}}.dropup .dropdown-menu{bottom:100%;margin-bottom:.125rem;margin-top:0;top:auto}.dropup .dropdown-toggle:after{border-bottom:.3em solid;border-left:.3em solid transparent;border-right:.3em solid transparent;border-top:0;content:"";display:inline-block;margin-left:.255em;vertical-align:.255em}.dropup .dropdown-toggle:empty:after{margin-left:0}.dropright .dropdown-menu{left:100%;margin-left:.125rem;margin-top:0;right:auto;top:0}.dropright .dropdown-toggle:after{border-bottom:.3em solid transparent;border-left:.3em solid;border-right:0;border-top:.3em solid transparent;content:"";display:inline-block;margin-left:.255em;vertical-align:.255em}.dropright .dropdown-toggle:empty:after{margin-left:0}.dropright .dropdown-toggle:after{vertical-align:0}.dropleft .dropdown-menu{left:auto;margin-right:.125rem;margin-top:0;right:100%;top:0}.dropleft .dropdown-toggle:after{content:"";display:inline-block;display:none;margin-left:.255em;vertical-align:.255em}.dropleft .dropdown-toggle:before{border-bottom:.3em solid transparent;border-right:.3em solid;border-top:.3em solid transparent;content:"";display:inline-block;margin-right:.255em;vertical-align:.255em}.dropleft .dropdown-toggle:empty:after{margin-left:0}.dropleft .dropdown-toggle:before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{bottom:auto;right:auto}.dropdown-divider{border-top:1px solid #e5e7eb;height:0;margin:.5rem 0;overflow:hidden}.dropdown-item{background-color:transparent;border:0;clear:both;color:#fff;display:block;font-weight:400;padding:.25rem 1.5rem;text-align:inherit;white-space:nowrap;width:100%}.dropdown-item:focus,.dropdown-item:hover{background-color:#e5e7eb;color:#090d15;text-decoration:none}.dropdown-item.active,.dropdown-item:active{background-color:#4040c8;color:#fff;text-decoration:none}.dropdown-item.disabled,.dropdown-item:disabled{background-color:transparent;color:#6b7280;pointer-events:none}.dropdown-menu.show{display:block}.dropdown-header{color:#4b5563;display:block;font-size:.875rem;margin-bottom:0;padding:.5rem 1.5rem;white-space:nowrap}.dropdown-item-text{color:#fff;display:block;padding:.25rem 1.5rem}.btn-group,.btn-group-vertical{display:inline-flex;position:relative;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{flex:1 1 auto;position:relative}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.dropdown-toggle-split{padding-left:.5625rem;padding-right:.5625rem}.dropdown-toggle-split:after,.dropright .dropdown-toggle-split:after,.dropup .dropdown-toggle-split:after{margin-left:0}.dropleft .dropdown-toggle-split:before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-left:.375rem;padding-right:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-left:.75rem;padding-right:.75rem}.btn-group-vertical{align-items:flex-start;flex-direction:column;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-left-radius:0;border-bottom-right-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{clip:rect(0,0,0,0);pointer-events:none;position:absolute}.input-group{align-items:stretch;display:flex;flex-wrap:wrap;position:relative;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{flex:1 1 auto;margin-bottom:0;min-width:0;position:relative;width:1%}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.input-group>.custom-file{align-items:center;display:flex}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label:after{border-bottom-right-radius:0;border-top-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-bottom-left-radius:0;border-top-left-radius:0}.input-group.has-validation>.custom-file:nth-last-child(n+3) .custom-file-label,.input-group.has-validation>.custom-file:nth-last-child(n+3) .custom-file-label:after,.input-group.has-validation>.custom-select:nth-last-child(n+3),.input-group.has-validation>.form-control:nth-last-child(n+3),.input-group:not(.has-validation)>.custom-file:not(:last-child) .custom-file-label,.input-group:not(.has-validation)>.custom-file:not(:last-child) .custom-file-label:after,.input-group:not(.has-validation)>.custom-select:not(:last-child),.input-group:not(.has-validation)>.form-control:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.input-group-append,.input-group-prepend{display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{align-items:center;background-color:#e5e7eb;border:1px solid #4b5563;border-radius:.25rem;color:#e5e7eb;display:flex;font-size:1rem;font-weight:400;line-height:1.5;margin-bottom:0;padding:.375rem .75rem;text-align:center;white-space:nowrap}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{border-radius:6px;font-size:1.25rem;line-height:1.5;padding:.5rem 1rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{border-radius:.2rem;font-size:.875rem;line-height:1.5;padding:.25rem .5rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group.has-validation>.input-group-append:nth-last-child(n+3)>.btn,.input-group.has-validation>.input-group-append:nth-last-child(n+3)>.input-group-text,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.btn,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-bottom-right-radius:0;border-top-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-bottom-left-radius:0;border-top-left-radius:0}.custom-control{display:block;min-height:1.5rem;padding-left:1.5rem;position:relative;-webkit-print-color-adjust:exact;print-color-adjust:exact;z-index:1}.custom-control-inline{display:inline-flex;margin-right:1rem}.custom-control-input{height:1.25rem;left:0;opacity:0;position:absolute;width:1rem;z-index:-1}.custom-control-input:checked~.custom-control-label:before{background-color:#4040c8;border-color:#4040c8;color:#fff}.custom-control-input:focus~.custom-control-label:before{box-shadow:0 0 0 .2rem rgba(64,64,200,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label:before{border-color:#a3a3e5}.custom-control-input:not(:disabled):active~.custom-control-label:before{background-color:#cbcbf0;border-color:#cbcbf0;color:#fff}.custom-control-input:disabled~.custom-control-label,.custom-control-input[disabled]~.custom-control-label{color:#4b5563}.custom-control-input:disabled~.custom-control-label:before,.custom-control-input[disabled]~.custom-control-label:before{background-color:#e5e7eb}.custom-control-label{margin-bottom:0;position:relative;vertical-align:top}.custom-control-label:before{background-color:#1f2937;border:1px solid #6b7280;pointer-events:none}.custom-control-label:after,.custom-control-label:before{content:"";display:block;height:1rem;left:-1.5rem;position:absolute;top:.25rem;width:1rem}.custom-control-label:after{background:50%/50% 50% no-repeat}.custom-checkbox .custom-control-label:before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath fill='%23fff' d='m6.564.75-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label:before{background-color:#4040c8;border-color:#4040c8}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label:before{background-color:rgba(64,64,200,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label:before{background-color:rgba(64,64,200,.5)}.custom-radio .custom-control-label:before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label:before{background-color:rgba(64,64,200,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label:before{border-radius:.5rem;left:-2.25rem;pointer-events:all;width:1.75rem}.custom-switch .custom-control-label:after{background-color:#6b7280;border-radius:.5rem;height:calc(1rem - 4px);left:calc(-2.25rem + 2px);top:calc(.25rem + 2px);transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;width:calc(1rem - 4px)}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label:after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label:after{background-color:#1f2937;transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label:before{background-color:rgba(64,64,200,.5)}.custom-select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#1f2937 url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5'%3E%3Cpath fill='%231f2937' d='M2 0 0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") right .75rem center/8px 10px no-repeat;border:1px solid #4b5563;border-radius:.25rem;color:#e5e7eb;display:inline-block;font-size:1rem;font-weight:400;height:calc(1.5em + .75rem + 2px);line-height:1.5;padding:.375rem 1.75rem .375rem .75rem;vertical-align:middle;width:100%}.custom-select:focus{border-color:#a3a3e5;box-shadow:0 0 0 .2rem rgba(64,64,200,.25);outline:0}.custom-select:focus::-ms-value{background-color:#1f2937;color:#e5e7eb}.custom-select[multiple],.custom-select[size]:not([size="1"]){background-image:none;height:auto;padding-right:.75rem}.custom-select:disabled{background-color:#e5e7eb;color:#4b5563}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #e5e7eb}.custom-select-sm{font-size:.875rem;height:calc(1.5em + .5rem + 2px);padding-bottom:.25rem;padding-left:.5rem;padding-top:.25rem}.custom-select-lg{font-size:1.25rem;height:calc(1.5em + 1rem + 2px);padding-bottom:.5rem;padding-left:1rem;padding-top:.5rem}.custom-file{display:inline-block;margin-bottom:0}.custom-file,.custom-file-input{height:calc(1.5em + .75rem + 2px);position:relative;width:100%}.custom-file-input{margin:0;opacity:0;overflow:hidden;z-index:2}.custom-file-input:focus~.custom-file-label{border-color:#a3a3e5;box-shadow:0 0 0 .2rem rgba(64,64,200,.25)}.custom-file-input:disabled~.custom-file-label,.custom-file-input[disabled]~.custom-file-label{background-color:#e5e7eb}.custom-file-input:lang(en)~.custom-file-label:after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]:after{content:attr(data-browse)}.custom-file-label{background-color:#1f2937;border:1px solid #4b5563;border-radius:.25rem;font-weight:400;height:calc(1.5em + .75rem + 2px);left:0;overflow:hidden;z-index:1}.custom-file-label,.custom-file-label:after{color:#e5e7eb;line-height:1.5;padding:.375rem .75rem;position:absolute;right:0;top:0}.custom-file-label:after{background-color:#e5e7eb;border-left:inherit;border-radius:0 .25rem .25rem 0;bottom:0;content:"Browse";display:block;height:calc(1.5em + .75rem);z-index:3}.custom-range{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;height:1.4rem;padding:0;width:100%}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #111827,0 0 0 .2rem rgba(64,64,200,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #111827,0 0 0 .2rem rgba(64,64,200,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #111827,0 0 0 .2rem rgba(64,64,200,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;background-color:#4040c8;border:0;border-radius:1rem;height:1rem;margin-top:-.25rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;width:1rem}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#cbcbf0}.custom-range::-webkit-slider-runnable-track{background-color:#d1d5db;border-color:transparent;border-radius:1rem;color:transparent;cursor:pointer;height:.5rem;width:100%}.custom-range::-moz-range-thumb{-moz-appearance:none;appearance:none;background-color:#4040c8;border:0;border-radius:1rem;height:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;width:1rem}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{-moz-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#cbcbf0}.custom-range::-moz-range-track{background-color:#d1d5db;border-color:transparent;border-radius:1rem;color:transparent;cursor:pointer;height:.5rem;width:100%}.custom-range::-ms-thumb{appearance:none;background-color:#4040c8;border:0;border-radius:1rem;height:1rem;margin-left:.2rem;margin-right:.2rem;margin-top:0;-ms-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;width:1rem}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{-ms-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#cbcbf0}.custom-range::-ms-track{background-color:transparent;border-color:transparent;border-width:.5rem;color:transparent;cursor:pointer;height:.5rem;width:100%}.custom-range::-ms-fill-lower,.custom-range::-ms-fill-upper{background-color:#d1d5db;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px}.custom-range:disabled::-webkit-slider-thumb{background-color:#6b7280}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#6b7280}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#6b7280}.custom-control-label:before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label:before,.custom-file-label,.custom-select{transition:none}}.nav{display:flex;flex-wrap:wrap;list-style:none;margin-bottom:0;padding-left:0}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#4b5563;cursor:default;pointer-events:none}.nav-tabs{border-bottom:1px solid #d1d5db}.nav-tabs .nav-link{background-color:transparent;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem;margin-bottom:-1px}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e5e7eb #e5e7eb #d1d5db;isolation:isolate}.nav-tabs .nav-link.disabled{background-color:transparent;border-color:transparent;color:#4b5563}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{background-color:#111827;border-color:#d1d5db #d1d5db #111827;color:#374151}.nav-tabs .dropdown-menu{border-top-left-radius:0;border-top-right-radius:0;margin-top:-1px}.nav-pills .nav-link{background:none;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{background-color:#1f2937;color:#fff}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{padding:.5rem 1rem;position:relative}.navbar,.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{align-items:center;display:flex;flex-wrap:wrap;justify-content:space-between}.navbar-brand{display:inline-block;font-size:1.25rem;line-height:inherit;margin-right:1rem;padding-bottom:.3125rem;padding-top:.3125rem;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:flex;flex-direction:column;list-style:none;margin-bottom:0;padding-left:0}.navbar-nav .nav-link{padding-left:0;padding-right:0}.navbar-nav .dropdown-menu{float:none;position:static}.navbar-text{display:inline-block;padding-bottom:.5rem;padding-top:.5rem}.navbar-collapse{align-items:center;flex-basis:100%;flex-grow:1}.navbar-toggler{background-color:transparent;border:1px solid transparent;border-radius:.25rem;font-size:1.25rem;line-height:1;padding:.25rem .75rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{background:50%/100% 100% no-repeat;content:"";display:inline-block;height:1.5em;vertical-align:middle;width:1.5em}.navbar-nav-scroll{max-height:75vh;overflow-y:auto}@media (max-width:1.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{padding-left:0;padding-right:0}}@media (min-width:2px){.navbar-expand-sm{flex-flow:row 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-left:.5rem;padding-right:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{flex-wrap:nowrap}.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}}@media (max-width:7.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{padding-left:0;padding-right:0}}@media (min-width:8px){.navbar-expand-md{flex-flow:row 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-left:.5rem;padding-right:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{flex-wrap:nowrap}.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}}@media (max-width:8.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{padding-left:0;padding-right:0}}@media (min-width:9px){.navbar-expand-lg{flex-flow:row 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-left:.5rem;padding-right:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{flex-wrap:nowrap}.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}}@media (max-width:9.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{padding-left:0;padding-right:0}}@media (min-width:10px){.navbar-expand-xl{flex-flow:row 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-left:.5rem;padding-right:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{flex-wrap:nowrap}.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{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{padding-left:0;padding-right:0}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-left:.5rem;padding-right:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{flex-wrap:nowrap}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand,.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{border-color:rgba(0,0,0,.1);color:rgba(0,0,0,.5)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand,.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:hsla(0,0%,100%,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:hsla(0,0%,100%,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:hsla(0,0%,100%,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{border-color:hsla(0,0%,100%,.1);color:hsla(0,0%,100%,.5)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-dark .navbar-text{color:hsla(0,0%,100%,.5)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{word-wrap:break-word;background-clip:border-box;background-color:#1f2937;border:1px solid rgba(0,0,0,.125);border-radius:6px;display:flex;flex-direction:column;min-width:0;position:relative}.card>hr{margin-left:0;margin-right:0}.card>.list-group{border-bottom:inherit;border-top:inherit}.card>.list-group:first-child{border-top-left-radius:5px;border-top-right-radius:5px;border-top-width:0}.card>.list-group:last-child{border-bottom-left-radius:5px;border-bottom-right-radius:5px;border-bottom-width:0}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem}.card-subtitle,.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{background-color:#374151;border-bottom:1px solid rgba(0,0,0,.125);margin-bottom:0;padding:.75rem 1.25rem}.card-header:first-child{border-radius:5px 5px 0 0}.card-footer{background-color:#374151;border-top:1px solid rgba(0,0,0,.125);padding:.75rem 1.25rem}.card-footer:last-child{border-radius:0 0 5px 5px}.card-header-tabs{border-bottom:0;margin-bottom:-.75rem}.card-header-pills,.card-header-tabs{margin-left:-.625rem;margin-right:-.625rem}.card-img-overlay{border-radius:5px;bottom:0;left:0;padding:1.25rem;position:absolute;right:0;top:0}.card-img,.card-img-bottom,.card-img-top{flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:5px;border-top-right-radius:5px}.card-img,.card-img-bottom{border-bottom-left-radius:5px;border-bottom-right-radius:5px}.card-deck .card{margin-bottom:15px}@media (min-width:2px){.card-deck{display:flex;flex-flow:row wrap;margin-left:-15px;margin-right:-15px}.card-deck .card{flex:1 0 0%;margin-bottom:0;margin-left:15px;margin-right:15px}}.card-group>.card{margin-bottom:15px}@media (min-width:2px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{border-left:0;margin-left:0}.card-group>.card:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:2px){.card-columns{-moz-column-count:3;column-count:3;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion{overflow-anchor:none}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-left-radius:0;border-bottom-right-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{background-color:#e5e7eb;border-radius:.25rem;display:flex;flex-wrap:wrap;list-style:none;margin-bottom:1rem;padding:.75rem 1rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item:before{color:#4b5563;content:"/";float:left;padding-right:.5rem}.breadcrumb-item+.breadcrumb-item:hover:before{text-decoration:underline;text-decoration:none}.breadcrumb-item.active{color:#4b5563}.pagination{border-radius:.25rem;display:flex;list-style:none;padding-left:0}.page-link{background-color:#fff;border:1px solid #d1d5db;color:#818cf8;display:block;line-height:1.25;margin-left:-1px;padding:.5rem .75rem;position:relative}.page-link:hover{background-color:#e5e7eb;border-color:#d1d5db;color:#a5b4fc;text-decoration:none;z-index:2}.page-link:focus{box-shadow:0 0 0 .2rem rgba(64,64,200,.25);outline:0;z-index:3}.page-item:first-child .page-link{border-bottom-left-radius:.25rem;border-top-left-radius:.25rem;margin-left:0}.page-item:last-child .page-link{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.page-item.active .page-link{background-color:#4040c8;border-color:#4040c8;color:#fff;z-index:3}.page-item.disabled .page-link{background-color:#fff;border-color:#d1d5db;color:#4b5563;cursor:auto;pointer-events:none}.pagination-lg .page-link{font-size:1.25rem;line-height:1.5;padding:.75rem 1.5rem}.pagination-lg .page-item:first-child .page-link{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg .page-item:last-child .page-link{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm .page-link{font-size:.875rem;line-height:1.5;padding:.25rem .5rem}.pagination-sm .page-item:first-child .page-link{border-bottom-left-radius:.2rem;border-top-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-bottom-right-radius:.2rem;border-top-right-radius:.2rem}.badge{border-radius:.25rem;display:inline-block;font-size:.875rem;font-weight:600;line-height:1;padding:.25em .4em;text-align:center;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:baseline;white-space:nowrap}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{border-radius:10rem;padding-left:.6em;padding-right:.6em}.badge-primary{background-color:#4040c8;color:#fff}a.badge-primary:focus,a.badge-primary:hover{background-color:#3030a5;color:#fff}a.badge-primary.focus,a.badge-primary:focus{box-shadow:0 0 0 .2rem rgba(64,64,200,.5);outline:0}.badge-secondary{background-color:#4b5563;color:#fff}a.badge-secondary:focus,a.badge-secondary:hover{background-color:#353c46;color:#fff}a.badge-secondary.focus,a.badge-secondary:focus{box-shadow:0 0 0 .2rem rgba(75,85,99,.5);outline:0}.badge-success{background-color:#059669}a.badge-success:focus,a.badge-success:hover{background-color:#036546;color:#fff}a.badge-success.focus,a.badge-success:focus{box-shadow:0 0 0 .2rem rgba(5,150,105,.5);outline:0}.badge-info{background-color:#2563eb}a.badge-info:focus,a.badge-info:hover{background-color:#134cca;color:#fff}a.badge-info.focus,a.badge-info:focus{box-shadow:0 0 0 .2rem rgba(37,99,235,.5);outline:0}.badge-warning{background-color:#d97706}a.badge-warning:focus,a.badge-warning:hover{background-color:#a75c05;color:#fff}a.badge-warning.focus,a.badge-warning:focus{box-shadow:0 0 0 .2rem rgba(217,119,6,.5);outline:0}.badge-danger{background-color:#dc2626}a.badge-danger:focus,a.badge-danger:hover{background-color:#b21d1d;color:#fff}a.badge-danger.focus,a.badge-danger:focus{box-shadow:0 0 0 .2rem rgba(220,38,38,.5);outline:0}.badge-light{background-color:#f3f4f6;color:#111827}a.badge-light:focus,a.badge-light:hover{background-color:#d6d9e0;color:#111827}a.badge-light.focus,a.badge-light:focus{box-shadow:0 0 0 .2rem rgba(243,244,246,.5);outline:0}.badge-dark{background-color:#1f2937;color:#fff}a.badge-dark:focus,a.badge-dark:hover{background-color:#0d1116;color:#fff}a.badge-dark.focus,a.badge-dark:focus{box-shadow:0 0 0 .2rem rgba(31,41,55,.5);outline:0}.jumbotron{background-color:#e5e7eb;border-radius:6px;margin-bottom:2rem;padding:2rem 1rem}@media (min-width:2px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{border-radius:0;padding-left:0;padding-right:0}.alert{border:1px solid transparent;border-radius:.25rem;margin-bottom:1rem;padding:.75rem 1.25rem;position:relative}.alert-heading{color:inherit}.alert-link{font-weight:600}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{color:inherit;padding:.75rem 1.25rem;position:absolute;right:0;top:0;z-index:2}.alert-primary{background-color:#d9d9f4;border-color:#cacaf0;color:#212168}.alert-primary hr{border-top-color:#b6b6ea}.alert-primary .alert-link{color:#151541}.alert-secondary{background-color:#dbdde0;border-color:#cdcfd3;color:#272c33}.alert-secondary hr{border-top-color:#bfc2c7}.alert-secondary .alert-link{color:#111316}.alert-success{background-color:#cdeae1;border-color:#b9e2d5;color:#034e37}.alert-success hr{border-top-color:#a7dbca}.alert-success .alert-link{color:#011d14}.alert-info{background-color:#d3e0fb;border-color:#c2d3f9;color:#13337a}.alert-info hr{border-top-color:#abc2f7}.alert-info .alert-link{color:#0c214e}.alert-warning{background-color:#f7e4cd;border-color:#f4d9b9;color:#713e03}.alert-warning hr{border-top-color:#f1cda3}.alert-warning .alert-link{color:#3f2302}.alert-danger{background-color:#f8d4d4;border-color:#f5c2c2;color:#721414}.alert-danger hr{border-top-color:#f1acac}.alert-danger .alert-link{color:#470c0c}.alert-light{background-color:#fdfdfd;border-color:#fcfcfc;color:#7e7f80}.alert-light hr{border-top-color:#efefef}.alert-light .alert-link{color:#656666}.alert-dark{background-color:#d2d4d7;border-color:#c0c3c7;color:#10151d}.alert-dark hr{border-top-color:#b3b6bb}.alert-dark .alert-link{color:#000}@keyframes progress-bar-stripes{0%{background-position:1rem 0}to{background-position:0 0}}.progress{background-color:#e5e7eb;border-radius:.25rem;font-size:.75rem;height:1rem;line-height:0}.progress,.progress-bar{display:flex;overflow:hidden}.progress-bar{background-color:#4040c8;color:#fff;flex-direction:column;justify-content:center;text-align:center;transition:width .6s ease;white-space:nowrap}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.15) 0,hsla(0,0%,100%,.15) 75%,transparent 0,transparent);background-size:1rem 1rem}.progress-bar-animated{animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.media{align-items:flex-start;display:flex}.media-body{flex:1}.list-group{border-radius:.25rem;display:flex;flex-direction:column;margin-bottom:0;padding-left:0}.list-group-item-action{color:#374151;text-align:inherit;width:100%}.list-group-item-action:focus,.list-group-item-action:hover{background-color:#f3f4f6;color:#374151;text-decoration:none;z-index:1}.list-group-item-action:active{background-color:#e5e7eb;color:#f3f4f6}.list-group-item{background-color:#fff;border:1px solid rgba(0,0,0,.125);display:block;padding:.75rem 1.25rem;position:relative}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-left-radius:inherit;border-bottom-right-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{background-color:#fff;color:#4b5563;pointer-events:none}.list-group-item.active{background-color:#4040c8;border-color:#4040c8;color:#fff;z-index:2}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{border-top-width:1px;margin-top:-1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-bottom-left-radius:0;border-top-right-radius:.25rem}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-left-width:0;border-top-width:1px}.list-group-horizontal>.list-group-item+.list-group-item.active{border-left-width:1px;margin-left:-1px}@media (min-width:2px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-bottom-left-radius:0;border-top-right-radius:.25rem}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-left-width:0;border-top-width:1px}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{border-left-width:1px;margin-left:-1px}}@media (min-width:8px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-bottom-left-radius:0;border-top-right-radius:.25rem}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-left-width:0;border-top-width:1px}.list-group-horizontal-md>.list-group-item+.list-group-item.active{border-left-width:1px;margin-left:-1px}}@media (min-width:9px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-bottom-left-radius:0;border-top-right-radius:.25rem}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-left-width:0;border-top-width:1px}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{border-left-width:1px;margin-left:-1px}}@media (min-width:10px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-bottom-left-radius:0;border-top-right-radius:.25rem}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-left-width:0;border-top-width:1px}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{border-left-width:1px;margin-left:-1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{background-color:#cacaf0;color:#212168}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{background-color:#b6b6ea;color:#212168}.list-group-item-primary.list-group-item-action.active{background-color:#212168;border-color:#212168;color:#fff}.list-group-item-secondary{background-color:#cdcfd3;color:#272c33}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{background-color:#bfc2c7;color:#272c33}.list-group-item-secondary.list-group-item-action.active{background-color:#272c33;border-color:#272c33;color:#fff}.list-group-item-success{background-color:#b9e2d5;color:#034e37}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{background-color:#a7dbca;color:#034e37}.list-group-item-success.list-group-item-action.active{background-color:#034e37;border-color:#034e37;color:#fff}.list-group-item-info{background-color:#c2d3f9;color:#13337a}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{background-color:#abc2f7;color:#13337a}.list-group-item-info.list-group-item-action.active{background-color:#13337a;border-color:#13337a;color:#fff}.list-group-item-warning{background-color:#f4d9b9;color:#713e03}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{background-color:#f1cda3;color:#713e03}.list-group-item-warning.list-group-item-action.active{background-color:#713e03;border-color:#713e03;color:#fff}.list-group-item-danger{background-color:#f5c2c2;color:#721414}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{background-color:#f1acac;color:#721414}.list-group-item-danger.list-group-item-action.active{background-color:#721414;border-color:#721414;color:#fff}.list-group-item-light{background-color:#fcfcfc;color:#7e7f80}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{background-color:#efefef;color:#7e7f80}.list-group-item-light.list-group-item-action.active{background-color:#7e7f80;border-color:#7e7f80;color:#fff}.list-group-item-dark{background-color:#c0c3c7;color:#10151d}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{background-color:#b3b6bb;color:#10151d}.list-group-item-dark.list-group-item-action.active{background-color:#10151d;border-color:#10151d;color:#fff}.close{color:#000;float:right;font-size:1.5rem;font-weight:600;line-height:1;opacity:.5;text-shadow:0 1px 0 #fff}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{background-color:transparent;border:0;padding:0}a.close.disabled{pointer-events:none}.toast{background-clip:padding-box;background-color:hsla(0,0%,100%,.85);border:1px solid rgba(0,0,0,.1);border-radius:.25rem;box-shadow:0 .25rem .75rem rgba(0,0,0,.1);flex-basis:350px;font-size:.875rem;max-width:350px;opacity:0}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{align-items:center;background-clip:padding-box;background-color:hsla(0,0%,100%,.85);border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px);color:#4b5563;display:flex;padding:.25rem .75rem}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{display:none;height:100%;left:0;outline:0;overflow:hidden;position:fixed;top:0;width:100%;z-index:1050}.modal-dialog{margin:.5rem;pointer-events:none;position:relative;width:auto}.modal.fade .modal-dialog{transform:translateY(-50px);transition:transform .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{display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{align-items:center;display:flex;min-height:calc(100% - 1rem)}.modal-dialog-centered:before{content:"";display:block;height:calc(100vh - 1rem);height:-moz-min-content;height:min-content}.modal-dialog-centered.modal-dialog-scrollable{flex-direction:column;height:100%;justify-content:center}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable:before{content:none}.modal-content{background-clip:padding-box;background-color:#1f2937;border:1px solid rgba(0,0,0,.2);border-radius:6px;display:flex;flex-direction:column;outline:0;pointer-events:auto;position:relative;width:100%}.modal-backdrop{background-color:#4b5563;height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:1040}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{align-items:flex-start;border-bottom:1px solid #4b5563;border-top-left-radius:5px;border-top-right-radius:5px;display:flex;justify-content:space-between;padding:1rem}.modal-header .close{margin:-1rem -1rem -1rem auto;padding:1rem}.modal-title{line-height:1.5;margin-bottom:0}.modal-body{flex:1 1 auto;padding:1rem;position:relative}.modal-footer{align-items:center;border-bottom-left-radius:5px;border-bottom-right-radius:5px;border-top:1px solid #4b5563;display:flex;flex-wrap:wrap;justify-content:flex-end;padding:.75rem}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{height:50px;overflow:scroll;position:absolute;top:-9999px;width:50px}@media (min-width:2px){.modal-dialog{margin:1.75rem auto;max-width:500px}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered:before{height:calc(100vh - 3.5rem);height:-moz-min-content;height:min-content}.modal-sm{max-width:300px}}@media (min-width:9px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:10px){.modal-xl{max-width:1140px}}.tooltip{word-wrap:break-word;display:block;font-family:Figtree,sans-serif;font-size:.875rem;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.5;margin:0;opacity:0;position:absolute;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;z-index:1070}.tooltip.show{opacity:.9}.tooltip .arrow{display:block;height:.4rem;position:absolute;width:.8rem}.tooltip .arrow:before{border-color:transparent;border-style:solid;content:"";position:absolute}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow:before,.bs-tooltip-top .arrow:before{border-top-color:#000;border-width:.4rem .4rem 0;top:0}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{height:.8rem;left:0;width:.4rem}.bs-tooltip-auto[x-placement^=right] .arrow:before,.bs-tooltip-right .arrow:before{border-right-color:#000;border-width:.4rem .4rem .4rem 0;right:0}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow:before,.bs-tooltip-bottom .arrow:before{border-bottom-color:#000;border-width:0 .4rem .4rem;bottom:0}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{height:.8rem;right:0;width:.4rem}.bs-tooltip-auto[x-placement^=left] .arrow:before,.bs-tooltip-left .arrow:before{border-left-color:#000;border-width:.4rem 0 .4rem .4rem;left:0}.tooltip-inner{background-color:#000;border-radius:.25rem;color:#fff;max-width:200px;padding:.25rem .5rem;text-align:center}.popover{word-wrap:break-word;background-clip:padding-box;background-color:#fff;border:1px solid rgba(0,0,0,.2);border-radius:6px;font-family:Figtree,sans-serif;font-size:.875rem;font-style:normal;font-weight:400;left:0;letter-spacing:normal;line-break:auto;line-height:1.5;max-width:276px;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;top:0;white-space:normal;word-break:normal;word-spacing:normal;z-index:1060}.popover,.popover .arrow{display:block;position:absolute}.popover .arrow{height:.5rem;margin:0 6px;width:1rem}.popover .arrow:after,.popover .arrow:before{border-color:transparent;border-style:solid;content:"";display:block;position:absolute}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=top]>.arrow:before,.bs-popover-top>.arrow:before{border-top-color:rgba(0,0,0,.25);border-width:.5rem .5rem 0;bottom:0}.bs-popover-auto[x-placement^=top]>.arrow:after,.bs-popover-top>.arrow:after{border-top-color:#fff;border-width:.5rem .5rem 0;bottom:1px}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{height:1rem;left:calc(-.5rem - 1px);margin:6px 0;width:.5rem}.bs-popover-auto[x-placement^=right]>.arrow:before,.bs-popover-right>.arrow:before{border-right-color:rgba(0,0,0,.25);border-width:.5rem .5rem .5rem 0;left:0}.bs-popover-auto[x-placement^=right]>.arrow:after,.bs-popover-right>.arrow:after{border-right-color:#fff;border-width:.5rem .5rem .5rem 0;left:1px}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=bottom]>.arrow:before,.bs-popover-bottom>.arrow:before{border-bottom-color:rgba(0,0,0,.25);border-width:0 .5rem .5rem;top:0}.bs-popover-auto[x-placement^=bottom]>.arrow:after,.bs-popover-bottom>.arrow:after{border-bottom-color:#fff;border-width:0 .5rem .5rem;top:1px}.bs-popover-auto[x-placement^=bottom] .popover-header:before,.bs-popover-bottom .popover-header:before{border-bottom:1px solid #f7f7f7;content:"";display:block;left:50%;margin-left:-.5rem;position:absolute;top:0;width:1rem}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{height:1rem;margin:6px 0;right:calc(-.5rem - 1px);width:.5rem}.bs-popover-auto[x-placement^=left]>.arrow:before,.bs-popover-left>.arrow:before{border-left-color:rgba(0,0,0,.25);border-width:.5rem 0 .5rem .5rem;right:0}.bs-popover-auto[x-placement^=left]>.arrow:after,.bs-popover-left>.arrow:after{border-left-color:#fff;border-width:.5rem 0 .5rem .5rem;right:1px}.popover-header{background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:5px;border-top-right-radius:5px;font-size:1rem;margin-bottom:0;padding:.5rem .75rem}.popover-header:empty{display:none}.popover-body{color:#f3f4f6;padding:.5rem .75rem}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{overflow:hidden;position:relative;width:100%}.carousel-inner:after{clear:both;content:"";display:block}.carousel-item{backface-visibility:hidden;display:none;float:left;margin-right:-100%;position:relative;transition:transform .6s ease-in-out;width:100%}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transform:none;transition-property:opacity}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{opacity:1;z-index:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{opacity:0;transition:opacity 0s .6s;z-index:0}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{align-items:center;background:none;border:0;bottom:0;color:#fff;display:flex;justify-content:center;opacity:.5;padding:0;position:absolute;text-align:center;top:0;transition:opacity .15s ease;width:15%;z-index:1}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;opacity:.9;outline:0;text-decoration:none}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{background:50%/100% 100% no-repeat;display:inline-block;height:20px;width:20px}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8'%3E%3Cpath d='m5.25 0-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8'%3E%3Cpath d='m2.75 0-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{bottom:0;display:flex;justify-content:center;left:0;list-style:none;margin-left:15%;margin-right:15%;padding-left:0;position:absolute;right:0;z-index:15}.carousel-indicators li{background-clip:padding-box;background-color:#fff;border-bottom:10px solid transparent;border-top:10px solid transparent;box-sizing:content-box;cursor:pointer;flex:0 1 auto;height:3px;margin-left:3px;margin-right:3px;opacity:.5;text-indent:-999px;transition:opacity .6s ease;width:30px}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{bottom:20px;color:#fff;left:15%;padding-bottom:20px;padding-top:20px;position:absolute;right:15%;text-align:center;z-index:10}@keyframes spinner-border{to{transform:rotate(1turn)}}.spinner-border{animation:spinner-border .75s linear infinite;border:.25em solid;border-radius:50%;border-right:.25em solid transparent;display:inline-block;height:2rem;vertical-align:-.125em;width:2rem}.spinner-border-sm{border-width:.2em;height:1rem;width:1rem}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{animation:spinner-grow .75s linear infinite;background-color:currentcolor;border-radius:50%;display:inline-block;height:2rem;opacity:0;vertical-align:-.125em;width:2rem}.spinner-grow-sm{height:1rem;width:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{animation-duration:1.5s}}.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}.bg-primary{background-color:#4040c8!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#3030a5!important}.bg-secondary{background-color:#4b5563!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#353c46!important}.bg-success{background-color:#059669!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#036546!important}.bg-info{background-color:#2563eb!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#134cca!important}.bg-warning{background-color:#d97706!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#a75c05!important}.bg-danger{background-color:#dc2626!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#b21d1d!important}.bg-light{background-color:#f3f4f6!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#d6d9e0!important}.bg-dark{background-color:#1f2937!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#0d1116!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #4b5563!important}.border-top{border-top:1px solid #4b5563!important}.border-right{border-right:1px solid #4b5563!important}.border-bottom{border-bottom:1px solid #4b5563!important}.border-left{border-left:1px solid #4b5563!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#4040c8!important}.border-secondary{border-color:#4b5563!important}.border-success{border-color:#059669!important}.border-info{border-color:#2563eb!important}.border-warning{border-color:#d97706!important}.border-danger{border-color:#dc2626!important}.border-light{border-color:#f3f4f6!important}.border-dark{border-color:#1f2937!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important}.rounded-right,.rounded-top{border-top-right-radius:.25rem!important}.rounded-bottom,.rounded-right{border-bottom-right-radius:.25rem!important}.rounded-bottom,.rounded-left{border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important}.rounded-lg{border-radius:6px!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix:after{clear:both;content:"";display:block}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!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}@media (min-width:2px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!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}}@media (min-width:8px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!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}}@media (min-width:9px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!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}}@media (min-width:10px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!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}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!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}}.embed-responsive{display:block;overflow:hidden;padding:0;position:relative;width:100%}.embed-responsive:before{content:"";display:block}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{border:0;bottom:0;height:100%;left:0;position:absolute;top:0;width:100%}.embed-responsive-21by9:before{padding-top:42.85714286%}.embed-responsive-16by9:before{padding-top:56.25%}.embed-responsive-4by3:before{padding-top:75%}.embed-responsive-1by1:before{padding-top:100%}.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-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-fill{flex:1 1 auto!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}.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}.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}@media (min-width:2px){.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-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-sm-fill{flex:1 1 auto!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}.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}.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}}@media (min-width:8px){.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-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-md-fill{flex:1 1 auto!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}.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}.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}}@media (min-width:9px){.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-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-lg-fill{flex:1 1 auto!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}.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}.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}}@media (min-width:10px){.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-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-xl-fill{flex:1 1 auto!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}.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}.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}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:2px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:8px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:9px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:10px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!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}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:sticky!important}.fixed-top{top:0}.fixed-bottom,.fixed-top{left:0;position:fixed;right:0;z-index:1030}.fixed-bottom{bottom:0}@supports (position:sticky){.sticky-top{position:sticky;top:0;z-index:1020}}.sr-only{clip:rect(0,0,0,0);border:0;height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;overflow:visible;position:static;white-space:normal;width:auto}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.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}.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}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:2px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:8px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:9px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:10px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.stretched-link:after{background-color:transparent;bottom:0;content:"";left:0;pointer-events:auto;position:absolute;right:0;top:0;z-index:1}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:2px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:8px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:9px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:10px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:600!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#4040c8!important}a.text-primary:focus,a.text-primary:hover{color:#2a2a92!important}.text-secondary{color:#4b5563!important}a.text-secondary:focus,a.text-secondary:hover{color:#2a3037!important}.text-success{color:#059669!important}a.text-success:focus,a.text-success:hover{color:#034c35!important}.text-info{color:#2563eb!important}a.text-info:focus,a.text-info:hover{color:#1043b3!important}.text-warning{color:#d97706!important}a.text-warning:focus,a.text-warning:hover{color:#8f4e04!important}.text-danger{color:#dc2626!important}a.text-danger:focus,a.text-danger:hover{color:#9c1919!important}.text-light{color:#f3f4f6!important}a.text-light:focus,a.text-light:hover{color:#c7ccd5!important}.text-dark{color:#1f2937!important}a.text-dark:focus,a.text-dark:hover{color:#030506!important}.text-body{color:#f3f4f6!important}.text-muted{color:#9ca3af!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:hsla(0,0%,100%,.5)!important}.text-hide{background-color:transparent;border:0;color:transparent;font:0/0 a;text-shadow:none}.text-decoration-none{text-decoration:none!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,:after,:before{box-shadow:none!important;text-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]:after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #6b7280}blockquote,img,pre,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}.container,body{min-width:9px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #d1d5db!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#374151}.table .thead-dark th{border-color:#374151;color:inherit}}.vjs-tree{color:#bfc7d5!important;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.vjs-tree .vjs-tree__content{border-left:1px dotted hsla(0,0%,80%,.28)!important}.vjs-tree .vjs-tree__node{cursor:pointer}.vjs-tree .vjs-tree__node:hover{color:#20a0ff}.vjs-tree .vjs-checkbox{left:-30px;position:absolute}.vjs-tree .vjs-value__boolean,.vjs-tree .vjs-value__null,.vjs-tree .vjs-value__number{color:#a291f5!important}.vjs-tree .vjs-value__string{color:#c3e88d!important}.vjs-tree .vjs-key{color:#c3cbd3!important}.hljs-addition,.hljs-attr,.hljs-keyword,.hljs-selector-tag{color:#13ce66}.hljs-bullet,.hljs-meta,.hljs-name,.hljs-string,.hljs-symbol,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable{color:#c3e88d}.hljs-comment,.hljs-deletion,.hljs-quote{color:#bfcbd9}.hljs-literal,.hljs-number,.hljs-title{color:#a291f5!important}body{padding-bottom:20px}.container{max-width:1440px}html{min-width:1140px}[v-cloak]{display:none}svg.icon{height:1rem;width:1rem}.header{border-bottom:1px solid #374151}.header .logo{color:#e5e7eb;text-decoration:none}.header .logo svg{height:1.7rem;width:1.7rem}.sidebar .nav-item a{border-radius:6px;color:#9ca3af;margin-bottom:4px;padding:.5rem .75rem}.sidebar .nav-item a svg{fill:#6b7280;height:1.25rem;margin-right:15px;width:1.25rem}.sidebar .nav-item a:hover{background-color:#1f2937;color:#d1d5db}.sidebar .nav-item a.active{background-color:#1f2937;color:#818cf8}.sidebar .nav-item a.active svg{fill:#6366f1}.card{border:none;box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1)}.card .bottom-radius{border-bottom-left-radius:6px;border-bottom-right-radius:6px}.card .card-header{background-color:#374151;border-bottom:none;min-height:60px;padding-bottom:.7rem;padding-top:.7rem}.card .card-header .btn-group .btn{padding:.2rem .5rem}.card .card-header .form-control-with-icon{position:relative}.card .card-header .form-control-with-icon .icon-wrapper{align-items:center;bottom:0;display:flex;justify-content:center;left:.75rem;position:absolute;top:0}.card .card-header .form-control-with-icon .icon-wrapper .icon{fill:#9ca3af}.card .card-header .form-control-with-icon .form-control{border-radius:9999px;font-size:.875rem;padding-left:2.25rem}.card .table td,.card .table th{padding:.75rem 1.25rem}.card .table th{background-color:#1f2937;border-bottom:0;font-size:.875rem;padding:.5rem 1.25rem}.card .table:not(.table-borderless) td{border-top:1px solid #374151}.card .table.penultimate-column-right td:nth-last-child(2),.card .table.penultimate-column-right th:nth-last-child(2){text-align:right}.card .table td.table-fit,.card .table th.table-fit{white-space:nowrap;width:1%}.fill-text-color{fill:#f3f4f6}.fill-danger{fill:#dc2626}.fill-warning{fill:#d97706}.fill-info{fill:#2563eb}.fill-success{fill:#059669}.fill-primary{fill:#4040c8}button:hover .fill-primary{fill:#fff}.btn-outline-primary.active .fill-primary{fill:#111827}.btn-outline-primary:not(:disabled):not(.disabled).active:focus{box-shadow:none!important}.btn-muted{background:#1f2937;color:#9ca3af}.btn-muted:focus,.btn-muted:hover{background:#374151;color:#d1d5db}.btn-muted.active{background:#4040c8;color:#fff}.badge-secondary{background:#d1d5db;color:#374151}.badge-success{background:#10b981;color:#fff}.badge-info{background:#3b82f6;color:#fff}.badge-warning{background:#f59e0b;color:#fff}.badge-danger{background:#ef4444;color:#fff}.control-action svg{fill:#6b7280;height:1.2rem;width:1.2rem}.control-action svg:hover{fill:#818cf8}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.spin{animation:spin 2s linear infinite}.card .nav-pills{background:#374151}.card .nav-pills .nav-link{border-radius:0;color:#9ca3af;font-size:.9rem;padding:.75rem 1.25rem}.card .nav-pills .nav-link:focus,.card .nav-pills .nav-link:hover{color:#e5e7eb}.card .nav-pills .nav-link.active{background:none;border-bottom:2px solid #a5b4fc;color:#a5b4fc}.list-enter-active:not(.dontanimate){transition:background 1s linear}.list-enter:not(.dontanimate),.list-leave-to:not(.dontanimate){background:#312e81}.code-bg .list-enter:not(.dontanimate),.code-bg .list-leave-to:not(.dontanimate){background:#4b5563}#indexScreen td{vertical-align:middle!important}.card-bg-secondary{background:#1f2937}.code-bg{background:#292d3e}.disabled-watcher{background:#dc2626;color:#fff;padding:.75rem}.copy-to-clipboard{--tw-text-opacity:1;color:rgb(231 232 242/var(--tw-text-opacity));opacity:.7;outline:2px solid transparent;outline-offset:2px;position:absolute;right:0;top:0;z-index:10} diff --git a/public/vendor/telescope/app.css b/public/vendor/telescope/app.css new file mode 100644 index 000000000..1f6375bf1 --- /dev/null +++ b/public/vendor/telescope/app.css @@ -0,0 +1,7 @@ +@charset "UTF-8"; +/*! + * Bootstrap v4.6.2 (https://getbootstrap.com/) + * Copyright 2011-2022 The Bootstrap Authors + * Copyright 2011-2022 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#4b5563;--gray-dark:#1f2937;--primary:#4040c8;--secondary:#4b5563;--success:#059669;--info:#2563eb;--warning:#d97706;--danger:#dc2626;--light:#f3f4f6;--dark:#1f2937;--breakpoint-xs:0;--breakpoint-sm:2px;--breakpoint-md:8px;--breakpoint-lg:9px;--breakpoint-xl:10px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,:after,:before{box-sizing:border-box}html{-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);font-family:sans-serif;line-height:1.15}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{background-color:#f3f4f6;color:#111827;font-family:Figtree,sans-serif;font-size:1rem;font-weight:400;line-height:1.5;margin:0;text-align:left}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;margin-top:0}p{margin-bottom:1rem;margin-top:0}abbr[data-original-title],abbr[title]{border-bottom:0;cursor:help;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{font-style:normal;line-height:inherit}address,dl,ol,ul{margin-bottom:1rem}dl,ol,ul{margin-top:0}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:600}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{background-color:transparent;color:#6366f1;text-decoration:none}a:hover{color:#4f46e5;text-decoration:underline}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}pre{-ms-overflow-style:scrollbar;margin-bottom:1rem;margin-top:0;overflow:auto}figure{margin:0 0 1rem}img{border-style:none}img,svg{vertical-align:middle}svg{overflow:hidden}table{border-collapse:collapse}caption{caption-side:bottom;color:#6b7280;padding-bottom:.75rem;padding-top:.75rem;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit;margin:0}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{border:0;margin:0;min-width:0;padding:0}legend{color:inherit;display:block;font-size:1.5rem;line-height:inherit;margin-bottom:.5rem;max-width:100%;padding:0;white-space:normal;width:100%}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:none;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}output{display:inline-block}summary{cursor:pointer;display:list-item}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-weight:500;line-height:1.2;margin-bottom:.5rem}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem}.display-1,.display-2{font-weight:300;line-height:1.2}.display-2{font-size:5.5rem}.display-3{font-size:4.5rem}.display-3,.display-4{font-weight:300;line-height:1.2}.display-4{font-size:3.5rem}hr{border:0;border-top:1px solid rgba(0,0,0,.1);margin-bottom:1rem;margin-top:1rem}.small,small{font-size:.875em;font-weight:400}.mark,mark{background-color:#fcf8e3;padding:.2em}.list-inline,.list-unstyled{list-style:none;padding-left:0}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{font-size:1.25rem;margin-bottom:1rem}.blockquote-footer{color:#4b5563;display:block;font-size:.875em}.blockquote-footer:before{content:"— "}.img-fluid,.img-thumbnail{height:auto;max-width:100%}.img-thumbnail{background-color:#f3f4f6;border:1px solid #d1d5db;border-radius:.25rem;padding:.25rem}.figure{display:inline-block}.figure-img{line-height:1;margin-bottom:.5rem}.figure-caption{color:#4b5563;font-size:90%}code{word-wrap:break-word;color:#e83e8c;font-size:87.5%}a>code{color:inherit}kbd{background-color:#111827;border-radius:.2rem;color:#fff;font-size:87.5%;padding:.2rem .4rem}kbd kbd{font-size:100%;font-weight:600;padding:0}pre{color:#111827;display:block;font-size:87.5%}pre code{color:inherit;font-size:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{margin-left:auto;margin-right:auto;padding-left:15px;padding-right:15px;width:100%}@media (min-width:2px){.container,.container-sm{max-width:1137px}}@media (min-width:8px){.container,.container-md,.container-sm{max-width:1138px}}@media (min-width:9px){.container,.container-lg,.container-md,.container-sm{max-width:1139px}}@media (min-width:10px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:flex;flex-wrap:wrap;margin-left:-15px;margin-right:-15px}.no-gutters{margin-left:0;margin-right:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-left:0;padding-right:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{padding-left:15px;padding-right:15px;position:relative;width:100%}.col{flex-basis:0;flex-grow:1;max-width:100%}.row-cols-1>*{flex:0 0 100%;max-width:100%}.row-cols-2>*{flex:0 0 50%;max-width:50%}.row-cols-3>*{flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-4>*{flex:0 0 25%;max-width:25%}.row-cols-5>*{flex:0 0 20%;max-width:20%}.row-cols-6>*{flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-auto{flex:0 0 auto;max-width:100%;width:auto}.col-1{flex:0 0 8.33333333%;max-width:8.33333333%}.col-2{flex:0 0 16.66666667%;max-width:16.66666667%}.col-3{flex:0 0 25%;max-width:25%}.col-4{flex:0 0 33.33333333%;max-width:33.33333333%}.col-5{flex:0 0 41.66666667%;max-width:41.66666667%}.col-6{flex:0 0 50%;max-width:50%}.col-7{flex:0 0 58.33333333%;max-width:58.33333333%}.col-8{flex:0 0 66.66666667%;max-width:66.66666667%}.col-9{flex:0 0 75%;max-width:75%}.col-10{flex:0 0 83.33333333%;max-width:83.33333333%}.col-11{flex:0 0 91.66666667%;max-width:91.66666667%}.col-12{flex:0 0 100%;max-width:100%}.order-first{order:-1}.order-last{order:13}.order-0{order:0}.order-1{order:1}.order-2{order:2}.order-3{order:3}.order-4{order:4}.order-5{order:5}.order-6{order:6}.order-7{order:7}.order-8{order:8}.order-9{order:9}.order-10{order:10}.order-11{order:11}.order-12{order:12}.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%}@media (min-width:2px){.col-sm{flex-basis:0;flex-grow:1;max-width:100%}.row-cols-sm-1>*{flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-sm-auto{flex:0 0 auto;max-width:100%;width:auto}.col-sm-1{flex:0 0 8.33333333%;max-width:8.33333333%}.col-sm-2{flex:0 0 16.66666667%;max-width:16.66666667%}.col-sm-3{flex:0 0 25%;max-width:25%}.col-sm-4{flex:0 0 33.33333333%;max-width:33.33333333%}.col-sm-5{flex:0 0 41.66666667%;max-width:41.66666667%}.col-sm-6{flex:0 0 50%;max-width:50%}.col-sm-7{flex:0 0 58.33333333%;max-width:58.33333333%}.col-sm-8{flex:0 0 66.66666667%;max-width:66.66666667%}.col-sm-9{flex:0 0 75%;max-width:75%}.col-sm-10{flex:0 0 83.33333333%;max-width:83.33333333%}.col-sm-11{flex:0 0 91.66666667%;max-width:91.66666667%}.col-sm-12{flex:0 0 100%;max-width:100%}.order-sm-first{order:-1}.order-sm-last{order:13}.order-sm-0{order:0}.order-sm-1{order:1}.order-sm-2{order:2}.order-sm-3{order:3}.order-sm-4{order:4}.order-sm-5{order:5}.order-sm-6{order:6}.order-sm-7{order:7}.order-sm-8{order:8}.order-sm-9{order:9}.order-sm-10{order:10}.order-sm-11{order:11}.order-sm-12{order:12}.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%}}@media (min-width:8px){.col-md{flex-basis:0;flex-grow:1;max-width:100%}.row-cols-md-1>*{flex:0 0 100%;max-width:100%}.row-cols-md-2>*{flex:0 0 50%;max-width:50%}.row-cols-md-3>*{flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-md-4>*{flex:0 0 25%;max-width:25%}.row-cols-md-5>*{flex:0 0 20%;max-width:20%}.row-cols-md-6>*{flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-md-auto{flex:0 0 auto;max-width:100%;width:auto}.col-md-1{flex:0 0 8.33333333%;max-width:8.33333333%}.col-md-2{flex:0 0 16.66666667%;max-width:16.66666667%}.col-md-3{flex:0 0 25%;max-width:25%}.col-md-4{flex:0 0 33.33333333%;max-width:33.33333333%}.col-md-5{flex:0 0 41.66666667%;max-width:41.66666667%}.col-md-6{flex:0 0 50%;max-width:50%}.col-md-7{flex:0 0 58.33333333%;max-width:58.33333333%}.col-md-8{flex:0 0 66.66666667%;max-width:66.66666667%}.col-md-9{flex:0 0 75%;max-width:75%}.col-md-10{flex:0 0 83.33333333%;max-width:83.33333333%}.col-md-11{flex:0 0 91.66666667%;max-width:91.66666667%}.col-md-12{flex:0 0 100%;max-width:100%}.order-md-first{order:-1}.order-md-last{order:13}.order-md-0{order:0}.order-md-1{order:1}.order-md-2{order:2}.order-md-3{order:3}.order-md-4{order:4}.order-md-5{order:5}.order-md-6{order:6}.order-md-7{order:7}.order-md-8{order:8}.order-md-9{order:9}.order-md-10{order:10}.order-md-11{order:11}.order-md-12{order:12}.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%}}@media (min-width:9px){.col-lg{flex-basis:0;flex-grow:1;max-width:100%}.row-cols-lg-1>*{flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-lg-auto{flex:0 0 auto;max-width:100%;width:auto}.col-lg-1{flex:0 0 8.33333333%;max-width:8.33333333%}.col-lg-2{flex:0 0 16.66666667%;max-width:16.66666667%}.col-lg-3{flex:0 0 25%;max-width:25%}.col-lg-4{flex:0 0 33.33333333%;max-width:33.33333333%}.col-lg-5{flex:0 0 41.66666667%;max-width:41.66666667%}.col-lg-6{flex:0 0 50%;max-width:50%}.col-lg-7{flex:0 0 58.33333333%;max-width:58.33333333%}.col-lg-8{flex:0 0 66.66666667%;max-width:66.66666667%}.col-lg-9{flex:0 0 75%;max-width:75%}.col-lg-10{flex:0 0 83.33333333%;max-width:83.33333333%}.col-lg-11{flex:0 0 91.66666667%;max-width:91.66666667%}.col-lg-12{flex:0 0 100%;max-width:100%}.order-lg-first{order:-1}.order-lg-last{order:13}.order-lg-0{order:0}.order-lg-1{order:1}.order-lg-2{order:2}.order-lg-3{order:3}.order-lg-4{order:4}.order-lg-5{order:5}.order-lg-6{order:6}.order-lg-7{order:7}.order-lg-8{order:8}.order-lg-9{order:9}.order-lg-10{order:10}.order-lg-11{order:11}.order-lg-12{order:12}.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%}}@media (min-width:10px){.col-xl{flex-basis:0;flex-grow:1;max-width:100%}.row-cols-xl-1>*{flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-xl-auto{flex:0 0 auto;max-width:100%;width:auto}.col-xl-1{flex:0 0 8.33333333%;max-width:8.33333333%}.col-xl-2{flex:0 0 16.66666667%;max-width:16.66666667%}.col-xl-3{flex:0 0 25%;max-width:25%}.col-xl-4{flex:0 0 33.33333333%;max-width:33.33333333%}.col-xl-5{flex:0 0 41.66666667%;max-width:41.66666667%}.col-xl-6{flex:0 0 50%;max-width:50%}.col-xl-7{flex:0 0 58.33333333%;max-width:58.33333333%}.col-xl-8{flex:0 0 66.66666667%;max-width:66.66666667%}.col-xl-9{flex:0 0 75%;max-width:75%}.col-xl-10{flex:0 0 83.33333333%;max-width:83.33333333%}.col-xl-11{flex:0 0 91.66666667%;max-width:91.66666667%}.col-xl-12{flex:0 0 100%;max-width:100%}.order-xl-first{order:-1}.order-xl-last{order:13}.order-xl-0{order:0}.order-xl-1{order:1}.order-xl-2{order:2}.order-xl-3{order:3}.order-xl-4{order:4}.order-xl-5{order:5}.order-xl-6{order:6}.order-xl-7{order:7}.order-xl-8{order:8}.order-xl-9{order:9}.order-xl-10{order:10}.order-xl-11{order:11}.order-xl-12{order:12}.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%}}.table{color:#111827;margin-bottom:1rem;width:100%}.table td,.table th{border-top:1px solid #e5e7eb;padding:.75rem;vertical-align:top}.table thead th{border-bottom:2px solid #e5e7eb;vertical-align:bottom}.table tbody+tbody{border-top:2px solid #e5e7eb}.table-sm td,.table-sm th{padding:.3rem}.table-bordered,.table-bordered td,.table-bordered th{border:1px solid #e5e7eb}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:#f3f4f6;color:#111827}.table-primary,.table-primary>td,.table-primary>th{background-color:#cacaf0}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#9c9ce2}.table-hover .table-primary:hover,.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#b6b6ea}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#cdcfd3}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#a1a7ae}.table-hover .table-secondary:hover,.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#bfc2c7}.table-success,.table-success>td,.table-success>th{background-color:#b9e2d5}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#7dc8b1}.table-hover .table-success:hover,.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#a7dbca}.table-info,.table-info>td,.table-info>th{background-color:#c2d3f9}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#8eaef5}.table-hover .table-info:hover,.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abc2f7}.table-warning,.table-warning>td,.table-warning>th{background-color:#f4d9b9}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ebb87e}.table-hover .table-warning:hover,.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#f1cda3}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c2c2}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed8e8e}.table-hover .table-danger:hover,.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1acac}.table-light,.table-light>td,.table-light>th{background-color:#fcfcfc}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#f9f9fa}.table-hover .table-light:hover,.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#efefef}.table-dark,.table-dark>td,.table-dark>th{background-color:#c0c3c7}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#8b9097}.table-hover .table-dark:hover,.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b3b6bb}.table-active,.table-active>td,.table-active>th{background-color:#f3f4f6}.table-hover .table-active:hover,.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:#e4e7eb}.table .thead-dark th{background-color:#1f2937;border-color:#2d3b4f;color:#fff}.table .thead-light th{background-color:#e5e7eb;border-color:#e5e7eb;color:#374151}.table-dark{background-color:#1f2937;color:#fff}.table-dark td,.table-dark th,.table-dark thead th{border-color:#2d3b4f}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:hsla(0,0%,100%,.05)}.table-dark.table-hover tbody tr:hover{background-color:hsla(0,0%,100%,.075);color:#fff}@media (max-width:1.98px){.table-responsive-sm{-webkit-overflow-scrolling:touch;display:block;overflow-x:auto;width:100%}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:7.98px){.table-responsive-md{-webkit-overflow-scrolling:touch;display:block;overflow-x:auto;width:100%}.table-responsive-md>.table-bordered{border:0}}@media (max-width:8.98px){.table-responsive-lg{-webkit-overflow-scrolling:touch;display:block;overflow-x:auto;width:100%}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:9.98px){.table-responsive-xl{-webkit-overflow-scrolling:touch;display:block;overflow-x:auto;width:100%}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{-webkit-overflow-scrolling:touch;display:block;overflow-x:auto;width:100%}.table-responsive>.table-bordered{border:0}.form-control{background-clip:padding-box;background-color:#fff;border:1px solid #d1d5db;border-radius:.25rem;color:#1f2937;display:block;font-size:1rem;font-weight:400;height:calc(1.5em + .75rem + 2px);line-height:1.5;padding:.375rem .75rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;width:100%}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{background-color:#fff;border-color:#a3a3e5;box-shadow:0 0 0 .2rem rgba(64,64,200,.25);color:#1f2937;outline:0}.form-control::-moz-placeholder{color:#4b5563;opacity:1}.form-control::placeholder{color:#4b5563;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e5e7eb;opacity:1}input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{-webkit-appearance:none;-moz-appearance:none;appearance:none}select.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #1f2937}select.form-control:focus::-ms-value{background-color:#fff;color:#1f2937}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{font-size:inherit;line-height:1.5;margin-bottom:0;padding-bottom:calc(.375rem + 1px);padding-top:calc(.375rem + 1px)}.col-form-label-lg{font-size:1.25rem;line-height:1.5;padding-bottom:calc(.5rem + 1px);padding-top:calc(.5rem + 1px)}.col-form-label-sm{font-size:.875rem;line-height:1.5;padding-bottom:calc(.25rem + 1px);padding-top:calc(.25rem + 1px)}.form-control-plaintext{background-color:transparent;border:solid transparent;border-width:1px 0;color:#111827;display:block;font-size:1rem;line-height:1.5;margin-bottom:0;padding:.375rem 0;width:100%}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-left:0;padding-right:0}.form-control-sm{border-radius:.2rem;font-size:.875rem;height:calc(1.5em + .5rem + 2px);line-height:1.5;padding:.25rem .5rem}.form-control-lg{border-radius:6px;font-size:1.25rem;height:calc(1.5em + 1rem + 2px);line-height:1.5;padding:.5rem 1rem}select.form-control[multiple],select.form-control[size],textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:flex;flex-wrap:wrap;margin-left:-5px;margin-right:-5px}.form-row>.col,.form-row>[class*=col-]{padding-left:5px;padding-right:5px}.form-check{display:block;padding-left:1.25rem;position:relative}.form-check-input{margin-left:-1.25rem;margin-top:.3rem;position:absolute}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{color:#6b7280}.form-check-label{margin-bottom:0}.form-check-inline{align-items:center;display:inline-flex;margin-right:.75rem;padding-left:0}.form-check-inline .form-check-input{margin-left:0;margin-right:.3125rem;margin-top:0;position:static}.valid-feedback{color:#059669;display:none;font-size:.875em;margin-top:.25rem;width:100%}.valid-tooltip{background-color:rgba(5,150,105,.9);border-radius:.25rem;color:#fff;display:none;font-size:.875rem;left:0;line-height:1.5;margin-top:.1rem;max-width:100%;padding:.25rem .5rem;position:absolute;top:100%;z-index:5}.form-row>.col>.valid-tooltip,.form-row>[class*=col-]>.valid-tooltip{left:5px}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath fill='%23059669' 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.1z'/%3E%3C/svg%3E");background-position:right calc(.375em + .1875rem) center;background-repeat:no-repeat;background-size:calc(.75em + .375rem) calc(.75em + .375rem);border-color:#059669;padding-right:calc(1.5em + .75rem)!important}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#059669;box-shadow:0 0 0 .2rem rgba(5,150,105,.25)}.was-validated select.form-control:valid,select.form-control.is-valid{background-position:right 1.5rem center;padding-right:3rem!important}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem);padding-right:calc(1.5em + .75rem)}.custom-select.is-valid,.was-validated .custom-select:valid{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5'%3E%3Cpath fill='%231f2937' d='M2 0 0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") right .75rem center/8px 10px no-repeat,#fff url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath fill='%23059669' 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.1z'/%3E%3C/svg%3E") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat;border-color:#059669;padding-right:calc(.75em + 2.3125rem)!important}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#059669;box-shadow:0 0 0 .2rem rgba(5,150,105,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#059669}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#059669}.custom-control-input.is-valid~.custom-control-label:before,.was-validated .custom-control-input:valid~.custom-control-label:before{border-color:#059669}.custom-control-input.is-valid:checked~.custom-control-label:before,.was-validated .custom-control-input:valid:checked~.custom-control-label:before{background-color:#07c78c;border-color:#07c78c}.custom-control-input.is-valid:focus~.custom-control-label:before,.was-validated .custom-control-input:valid:focus~.custom-control-label:before{box-shadow:0 0 0 .2rem rgba(5,150,105,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label:before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label:before{border-color:#059669}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#059669}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#059669;box-shadow:0 0 0 .2rem rgba(5,150,105,.25)}.invalid-feedback{color:#dc2626;display:none;font-size:.875em;margin-top:.25rem;width:100%}.invalid-tooltip{background-color:rgba(220,38,38,.9);border-radius:.25rem;color:#fff;display:none;font-size:.875rem;left:0;line-height:1.5;margin-top:.1rem;max-width:100%;padding:.25rem .5rem;position:absolute;top:100%;z-index:5}.form-row>.col>.invalid-tooltip,.form-row>[class*=col-]>.invalid-tooltip{left:5px}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc2626'%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='%23dc2626' stroke='none'/%3E%3C/svg%3E");background-position:right calc(.375em + .1875rem) center;background-repeat:no-repeat;background-size:calc(.75em + .375rem) calc(.75em + .375rem);border-color:#dc2626;padding-right:calc(1.5em + .75rem)!important}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc2626;box-shadow:0 0 0 .2rem rgba(220,38,38,.25)}.was-validated select.form-control:invalid,select.form-control.is-invalid{background-position:right 1.5rem center;padding-right:3rem!important}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem);padding-right:calc(1.5em + .75rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5'%3E%3Cpath fill='%231f2937' d='M2 0 0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") right .75rem center/8px 10px no-repeat,#fff url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc2626'%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='%23dc2626' stroke='none'/%3E%3C/svg%3E") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat;border-color:#dc2626;padding-right:calc(.75em + 2.3125rem)!important}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc2626;box-shadow:0 0 0 .2rem rgba(220,38,38,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc2626}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc2626}.custom-control-input.is-invalid~.custom-control-label:before,.was-validated .custom-control-input:invalid~.custom-control-label:before{border-color:#dc2626}.custom-control-input.is-invalid:checked~.custom-control-label:before,.was-validated .custom-control-input:invalid:checked~.custom-control-label:before{background-color:#e35252;border-color:#e35252}.custom-control-input.is-invalid:focus~.custom-control-label:before,.was-validated .custom-control-input:invalid:focus~.custom-control-label:before{box-shadow:0 0 0 .2rem rgba(220,38,38,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label:before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label:before{border-color:#dc2626}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc2626}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc2626;box-shadow:0 0 0 .2rem rgba(220,38,38,.25)}.form-inline{align-items:center;display:flex;flex-flow:row wrap}.form-inline .form-check{width:100%}@media (min-width:2px){.form-inline label{justify-content:center}.form-inline .form-group,.form-inline label{align-items:center;display:flex;margin-bottom:0}.form-inline .form-group{flex:0 0 auto;flex-flow:row wrap}.form-inline .form-control{display:inline-block;vertical-align:middle;width:auto}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{align-items:center;display:flex;justify-content:center;padding-left:0;width:auto}.form-inline .form-check-input{flex-shrink:0;margin-left:0;margin-right:.25rem;margin-top:0;position:relative}.form-inline .custom-control{align-items:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;color:#111827;display:inline-block;font-size:1rem;font-weight:400;line-height:1.5;padding:.375rem .75rem;text-align:center;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#111827;text-decoration:none}.btn.focus,.btn:focus{box-shadow:0 0 0 .2rem rgba(64,64,200,.25);outline:0}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{background-color:#4040c8;border-color:#4040c8;color:#fff}.btn-primary.focus,.btn-primary:focus,.btn-primary:hover{background-color:#3232af;border-color:#3030a5;color:#fff}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 0 rgba(93,93,208,.5)}.btn-primary.disabled,.btn-primary:disabled{background-color:#4040c8;border-color:#4040c8;color:#fff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{background-color:#3030a5;border-color:#2d2d9b;color:#fff}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(93,93,208,.5)}.btn-secondary{background-color:#4b5563;border-color:#4b5563;color:#fff}.btn-secondary.focus,.btn-secondary:focus,.btn-secondary:hover{background-color:#3b424d;border-color:#353c46;color:#fff}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 0 hsla(213,9%,44%,.5)}.btn-secondary.disabled,.btn-secondary:disabled{background-color:#4b5563;border-color:#4b5563;color:#fff}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{background-color:#353c46;border-color:#30363f;color:#fff}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 0 hsla(213,9%,44%,.5)}.btn-success{background-color:#059669;border-color:#059669;color:#fff}.btn-success.focus,.btn-success:focus,.btn-success:hover{background-color:#04714f;border-color:#036546;color:#fff}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 0 rgba(43,166,128,.5)}.btn-success.disabled,.btn-success:disabled{background-color:#059669;border-color:#059669;color:#fff}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{background-color:#036546;border-color:#03583e;color:#fff}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(43,166,128,.5)}.btn-info{background-color:#2563eb;border-color:#2563eb;color:#fff}.btn-info.focus,.btn-info:focus,.btn-info:hover{background-color:#1451d6;border-color:#134cca;color:#fff}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 0 rgba(70,122,238,.5)}.btn-info.disabled,.btn-info:disabled{background-color:#2563eb;border-color:#2563eb;color:#fff}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{background-color:#134cca;border-color:#1248bf;color:#fff}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(70,122,238,.5)}.btn-warning{background-color:#d97706;border-color:#d97706;color:#fff}.btn-warning.focus,.btn-warning:focus,.btn-warning:hover{background-color:#b46305;border-color:#a75c05;color:#fff}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 0 rgba(223,139,43,.5)}.btn-warning.disabled,.btn-warning:disabled{background-color:#d97706;border-color:#d97706;color:#fff}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{background-color:#a75c05;border-color:#9b5504;color:#fff}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(223,139,43,.5)}.btn-danger{background-color:#dc2626;border-color:#dc2626;color:#fff}.btn-danger.focus,.btn-danger:focus,.btn-danger:hover{background-color:#bd1f1f;border-color:#b21d1d;color:#fff}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 0 rgba(225,71,71,.5)}.btn-danger.disabled,.btn-danger:disabled{background-color:#dc2626;border-color:#dc2626;color:#fff}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{background-color:#b21d1d;border-color:#a71b1b;color:#fff}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(225,71,71,.5)}.btn-light{background-color:#f3f4f6;border-color:#f3f4f6;color:#111827}.btn-light.focus,.btn-light:focus,.btn-light:hover{background-color:#dde0e6;border-color:#d6d9e0;color:#111827}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 0 hsla(220,7%,83%,.5)}.btn-light.disabled,.btn-light:disabled{background-color:#f3f4f6;border-color:#f3f4f6;color:#111827}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{background-color:#d6d9e0;border-color:#cfd3db;color:#111827}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 0 hsla(220,7%,83%,.5)}.btn-dark{background-color:#1f2937;border-color:#1f2937;color:#fff}.btn-dark.focus,.btn-dark:focus,.btn-dark:hover{background-color:#11171f;border-color:#0d1116;color:#fff}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 0 rgba(65,73,85,.5)}.btn-dark.disabled,.btn-dark:disabled{background-color:#1f2937;border-color:#1f2937;color:#fff}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{background-color:#0d1116;border-color:#080b0e;color:#fff}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(65,73,85,.5)}.btn-outline-primary{border-color:#4040c8;color:#4040c8}.btn-outline-primary:hover{background-color:#4040c8;border-color:#4040c8;color:#fff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 0 rgba(64,64,200,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{background-color:transparent;color:#4040c8}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{background-color:#4040c8;border-color:#4040c8;color:#fff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(64,64,200,.5)}.btn-outline-secondary{border-color:#4b5563;color:#4b5563}.btn-outline-secondary:hover{background-color:#4b5563;border-color:#4b5563;color:#fff}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 0 rgba(75,85,99,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{background-color:transparent;color:#4b5563}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{background-color:#4b5563;border-color:#4b5563;color:#fff}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(75,85,99,.5)}.btn-outline-success{border-color:#059669;color:#059669}.btn-outline-success:hover{background-color:#059669;border-color:#059669;color:#fff}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 0 rgba(5,150,105,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{background-color:transparent;color:#059669}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{background-color:#059669;border-color:#059669;color:#fff}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(5,150,105,.5)}.btn-outline-info{border-color:#2563eb;color:#2563eb}.btn-outline-info:hover{background-color:#2563eb;border-color:#2563eb;color:#fff}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 0 rgba(37,99,235,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{background-color:transparent;color:#2563eb}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{background-color:#2563eb;border-color:#2563eb;color:#fff}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(37,99,235,.5)}.btn-outline-warning{border-color:#d97706;color:#d97706}.btn-outline-warning:hover{background-color:#d97706;border-color:#d97706;color:#fff}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 0 rgba(217,119,6,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{background-color:transparent;color:#d97706}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{background-color:#d97706;border-color:#d97706;color:#fff}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(217,119,6,.5)}.btn-outline-danger{border-color:#dc2626;color:#dc2626}.btn-outline-danger:hover{background-color:#dc2626;border-color:#dc2626;color:#fff}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 0 rgba(220,38,38,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{background-color:transparent;color:#dc2626}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{background-color:#dc2626;border-color:#dc2626;color:#fff}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(220,38,38,.5)}.btn-outline-light{border-color:#f3f4f6;color:#f3f4f6}.btn-outline-light:hover{background-color:#f3f4f6;border-color:#f3f4f6;color:#111827}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 0 rgba(243,244,246,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{background-color:transparent;color:#f3f4f6}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{background-color:#f3f4f6;border-color:#f3f4f6;color:#111827}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(243,244,246,.5)}.btn-outline-dark{border-color:#1f2937;color:#1f2937}.btn-outline-dark:hover{background-color:#1f2937;border-color:#1f2937;color:#fff}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 0 rgba(31,41,55,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{background-color:transparent;color:#1f2937}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{background-color:#1f2937;border-color:#1f2937;color:#fff}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(31,41,55,.5)}.btn-link{color:#6366f1;font-weight:400;text-decoration:none}.btn-link:hover{color:#4f46e5}.btn-link.focus,.btn-link:focus,.btn-link:hover{text-decoration:underline}.btn-link.disabled,.btn-link:disabled{color:#4b5563;pointer-events:none}.btn-group-lg>.btn,.btn-lg{border-radius:6px;font-size:1.25rem;line-height:1.5;padding:.5rem 1rem}.btn-group-sm>.btn,.btn-sm{border-radius:.2rem;font-size:.875rem;line-height:1.5;padding:.25rem .5rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .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;position:relative;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.width{height:auto;transition:width .35s ease;width:0}@media (prefers-reduced-motion:reduce){.collapsing.width{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle:after{border-bottom:0;border-left:.3em solid transparent;border-right:.3em solid transparent;border-top:.3em solid;content:"";display:inline-block;margin-left:.255em;vertical-align:.255em}.dropdown-toggle:empty:after{margin-left:0}.dropdown-menu{background-clip:padding-box;background-color:#fff;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;color:#111827;display:none;float:left;font-size:1rem;left:0;list-style:none;margin:.125rem 0 0;min-width:10rem;padding:.5rem 0;position:absolute;text-align:left;top:100%;z-index:1000}.dropdown-menu-left{left:0;right:auto}.dropdown-menu-right{left:auto;right:0}@media (min-width:2px){.dropdown-menu-sm-left{left:0;right:auto}.dropdown-menu-sm-right{left:auto;right:0}}@media (min-width:8px){.dropdown-menu-md-left{left:0;right:auto}.dropdown-menu-md-right{left:auto;right:0}}@media (min-width:9px){.dropdown-menu-lg-left{left:0;right:auto}.dropdown-menu-lg-right{left:auto;right:0}}@media (min-width:10px){.dropdown-menu-xl-left{left:0;right:auto}.dropdown-menu-xl-right{left:auto;right:0}}.dropup .dropdown-menu{bottom:100%;margin-bottom:.125rem;margin-top:0;top:auto}.dropup .dropdown-toggle:after{border-bottom:.3em solid;border-left:.3em solid transparent;border-right:.3em solid transparent;border-top:0;content:"";display:inline-block;margin-left:.255em;vertical-align:.255em}.dropup .dropdown-toggle:empty:after{margin-left:0}.dropright .dropdown-menu{left:100%;margin-left:.125rem;margin-top:0;right:auto;top:0}.dropright .dropdown-toggle:after{border-bottom:.3em solid transparent;border-left:.3em solid;border-right:0;border-top:.3em solid transparent;content:"";display:inline-block;margin-left:.255em;vertical-align:.255em}.dropright .dropdown-toggle:empty:after{margin-left:0}.dropright .dropdown-toggle:after{vertical-align:0}.dropleft .dropdown-menu{left:auto;margin-right:.125rem;margin-top:0;right:100%;top:0}.dropleft .dropdown-toggle:after{content:"";display:inline-block;display:none;margin-left:.255em;vertical-align:.255em}.dropleft .dropdown-toggle:before{border-bottom:.3em solid transparent;border-right:.3em solid;border-top:.3em solid transparent;content:"";display:inline-block;margin-right:.255em;vertical-align:.255em}.dropleft .dropdown-toggle:empty:after{margin-left:0}.dropleft .dropdown-toggle:before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{bottom:auto;right:auto}.dropdown-divider{border-top:1px solid #e5e7eb;height:0;margin:.5rem 0;overflow:hidden}.dropdown-item{background-color:transparent;border:0;clear:both;color:#374151;display:block;font-weight:400;padding:.25rem 1.5rem;text-align:inherit;white-space:nowrap;width:100%}.dropdown-item:focus,.dropdown-item:hover{background-color:#e5e7eb;color:#090d15;text-decoration:none}.dropdown-item.active,.dropdown-item:active{background-color:#4040c8;color:#fff;text-decoration:none}.dropdown-item.disabled,.dropdown-item:disabled{background-color:transparent;color:#6b7280;pointer-events:none}.dropdown-menu.show{display:block}.dropdown-header{color:#4b5563;display:block;font-size:.875rem;margin-bottom:0;padding:.5rem 1.5rem;white-space:nowrap}.dropdown-item-text{color:#374151;display:block;padding:.25rem 1.5rem}.btn-group,.btn-group-vertical{display:inline-flex;position:relative;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{flex:1 1 auto;position:relative}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.dropdown-toggle-split{padding-left:.5625rem;padding-right:.5625rem}.dropdown-toggle-split:after,.dropright .dropdown-toggle-split:after,.dropup .dropdown-toggle-split:after{margin-left:0}.dropleft .dropdown-toggle-split:before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-left:.375rem;padding-right:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-left:.75rem;padding-right:.75rem}.btn-group-vertical{align-items:flex-start;flex-direction:column;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-left-radius:0;border-bottom-right-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{clip:rect(0,0,0,0);pointer-events:none;position:absolute}.input-group{align-items:stretch;display:flex;flex-wrap:wrap;position:relative;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{flex:1 1 auto;margin-bottom:0;min-width:0;position:relative;width:1%}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.input-group>.custom-file{align-items:center;display:flex}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label:after{border-bottom-right-radius:0;border-top-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-bottom-left-radius:0;border-top-left-radius:0}.input-group.has-validation>.custom-file:nth-last-child(n+3) .custom-file-label,.input-group.has-validation>.custom-file:nth-last-child(n+3) .custom-file-label:after,.input-group.has-validation>.custom-select:nth-last-child(n+3),.input-group.has-validation>.form-control:nth-last-child(n+3),.input-group:not(.has-validation)>.custom-file:not(:last-child) .custom-file-label,.input-group:not(.has-validation)>.custom-file:not(:last-child) .custom-file-label:after,.input-group:not(.has-validation)>.custom-select:not(:last-child),.input-group:not(.has-validation)>.form-control:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.input-group-append,.input-group-prepend{display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{align-items:center;background-color:#e5e7eb;border:1px solid #d1d5db;border-radius:.25rem;color:#1f2937;display:flex;font-size:1rem;font-weight:400;line-height:1.5;margin-bottom:0;padding:.375rem .75rem;text-align:center;white-space:nowrap}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{border-radius:6px;font-size:1.25rem;line-height:1.5;padding:.5rem 1rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{border-radius:.2rem;font-size:.875rem;line-height:1.5;padding:.25rem .5rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group.has-validation>.input-group-append:nth-last-child(n+3)>.btn,.input-group.has-validation>.input-group-append:nth-last-child(n+3)>.input-group-text,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.btn,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-bottom-right-radius:0;border-top-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-bottom-left-radius:0;border-top-left-radius:0}.custom-control{display:block;min-height:1.5rem;padding-left:1.5rem;position:relative;-webkit-print-color-adjust:exact;print-color-adjust:exact;z-index:1}.custom-control-inline{display:inline-flex;margin-right:1rem}.custom-control-input{height:1.25rem;left:0;opacity:0;position:absolute;width:1rem;z-index:-1}.custom-control-input:checked~.custom-control-label:before{background-color:#4040c8;border-color:#4040c8;color:#fff}.custom-control-input:focus~.custom-control-label:before{box-shadow:0 0 0 .2rem rgba(64,64,200,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label:before{border-color:#a3a3e5}.custom-control-input:not(:disabled):active~.custom-control-label:before{background-color:#cbcbf0;border-color:#cbcbf0;color:#fff}.custom-control-input:disabled~.custom-control-label,.custom-control-input[disabled]~.custom-control-label{color:#4b5563}.custom-control-input:disabled~.custom-control-label:before,.custom-control-input[disabled]~.custom-control-label:before{background-color:#e5e7eb}.custom-control-label{margin-bottom:0;position:relative;vertical-align:top}.custom-control-label:before{background-color:#fff;border:1px solid #6b7280;pointer-events:none}.custom-control-label:after,.custom-control-label:before{content:"";display:block;height:1rem;left:-1.5rem;position:absolute;top:.25rem;width:1rem}.custom-control-label:after{background:50%/50% 50% no-repeat}.custom-checkbox .custom-control-label:before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath fill='%23fff' d='m6.564.75-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label:before{background-color:#4040c8;border-color:#4040c8}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label:before{background-color:rgba(64,64,200,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label:before{background-color:rgba(64,64,200,.5)}.custom-radio .custom-control-label:before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label:before{background-color:rgba(64,64,200,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label:before{border-radius:.5rem;left:-2.25rem;pointer-events:all;width:1.75rem}.custom-switch .custom-control-label:after{background-color:#6b7280;border-radius:.5rem;height:calc(1rem - 4px);left:calc(-2.25rem + 2px);top:calc(.25rem + 2px);transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;width:calc(1rem - 4px)}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label:after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label:after{background-color:#fff;transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label:before{background-color:rgba(64,64,200,.5)}.custom-select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#fff url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5'%3E%3Cpath fill='%231f2937' d='M2 0 0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") right .75rem center/8px 10px no-repeat;border:1px solid #d1d5db;border-radius:.25rem;color:#1f2937;display:inline-block;font-size:1rem;font-weight:400;height:calc(1.5em + .75rem + 2px);line-height:1.5;padding:.375rem 1.75rem .375rem .75rem;vertical-align:middle;width:100%}.custom-select:focus{border-color:#a3a3e5;box-shadow:0 0 0 .2rem rgba(64,64,200,.25);outline:0}.custom-select:focus::-ms-value{background-color:#fff;color:#1f2937}.custom-select[multiple],.custom-select[size]:not([size="1"]){background-image:none;height:auto;padding-right:.75rem}.custom-select:disabled{background-color:#e5e7eb;color:#4b5563}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #1f2937}.custom-select-sm{font-size:.875rem;height:calc(1.5em + .5rem + 2px);padding-bottom:.25rem;padding-left:.5rem;padding-top:.25rem}.custom-select-lg{font-size:1.25rem;height:calc(1.5em + 1rem + 2px);padding-bottom:.5rem;padding-left:1rem;padding-top:.5rem}.custom-file{display:inline-block;margin-bottom:0}.custom-file,.custom-file-input{height:calc(1.5em + .75rem + 2px);position:relative;width:100%}.custom-file-input{margin:0;opacity:0;overflow:hidden;z-index:2}.custom-file-input:focus~.custom-file-label{border-color:#a3a3e5;box-shadow:0 0 0 .2rem rgba(64,64,200,.25)}.custom-file-input:disabled~.custom-file-label,.custom-file-input[disabled]~.custom-file-label{background-color:#e5e7eb}.custom-file-input:lang(en)~.custom-file-label:after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]:after{content:attr(data-browse)}.custom-file-label{background-color:#fff;border:1px solid #d1d5db;border-radius:.25rem;font-weight:400;height:calc(1.5em + .75rem + 2px);left:0;overflow:hidden;z-index:1}.custom-file-label,.custom-file-label:after{color:#1f2937;line-height:1.5;padding:.375rem .75rem;position:absolute;right:0;top:0}.custom-file-label:after{background-color:#e5e7eb;border-left:inherit;border-radius:0 .25rem .25rem 0;bottom:0;content:"Browse";display:block;height:calc(1.5em + .75rem);z-index:3}.custom-range{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;height:1.4rem;padding:0;width:100%}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #f3f4f6,0 0 0 .2rem rgba(64,64,200,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #f3f4f6,0 0 0 .2rem rgba(64,64,200,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #f3f4f6,0 0 0 .2rem rgba(64,64,200,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;background-color:#4040c8;border:0;border-radius:1rem;height:1rem;margin-top:-.25rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;width:1rem}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#cbcbf0}.custom-range::-webkit-slider-runnable-track{background-color:#d1d5db;border-color:transparent;border-radius:1rem;color:transparent;cursor:pointer;height:.5rem;width:100%}.custom-range::-moz-range-thumb{-moz-appearance:none;appearance:none;background-color:#4040c8;border:0;border-radius:1rem;height:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;width:1rem}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{-moz-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#cbcbf0}.custom-range::-moz-range-track{background-color:#d1d5db;border-color:transparent;border-radius:1rem;color:transparent;cursor:pointer;height:.5rem;width:100%}.custom-range::-ms-thumb{appearance:none;background-color:#4040c8;border:0;border-radius:1rem;height:1rem;margin-left:.2rem;margin-right:.2rem;margin-top:0;-ms-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;width:1rem}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{-ms-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#cbcbf0}.custom-range::-ms-track{background-color:transparent;border-color:transparent;border-width:.5rem;color:transparent;cursor:pointer;height:.5rem;width:100%}.custom-range::-ms-fill-lower,.custom-range::-ms-fill-upper{background-color:#d1d5db;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px}.custom-range:disabled::-webkit-slider-thumb{background-color:#6b7280}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#6b7280}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#6b7280}.custom-control-label:before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label:before,.custom-file-label,.custom-select{transition:none}}.nav{display:flex;flex-wrap:wrap;list-style:none;margin-bottom:0;padding-left:0}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#4b5563;cursor:default;pointer-events:none}.nav-tabs{border-bottom:1px solid #d1d5db}.nav-tabs .nav-link{background-color:transparent;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem;margin-bottom:-1px}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e5e7eb #e5e7eb #d1d5db;isolation:isolate}.nav-tabs .nav-link.disabled{background-color:transparent;border-color:transparent;color:#4b5563}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{background-color:#f3f4f6;border-color:#d1d5db #d1d5db #f3f4f6;color:#374151}.nav-tabs .dropdown-menu{border-top-left-radius:0;border-top-right-radius:0;margin-top:-1px}.nav-pills .nav-link{background:none;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{background-color:#e5e7eb;color:#fff}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{padding:.5rem 1rem;position:relative}.navbar,.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{align-items:center;display:flex;flex-wrap:wrap;justify-content:space-between}.navbar-brand{display:inline-block;font-size:1.25rem;line-height:inherit;margin-right:1rem;padding-bottom:.3125rem;padding-top:.3125rem;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:flex;flex-direction:column;list-style:none;margin-bottom:0;padding-left:0}.navbar-nav .nav-link{padding-left:0;padding-right:0}.navbar-nav .dropdown-menu{float:none;position:static}.navbar-text{display:inline-block;padding-bottom:.5rem;padding-top:.5rem}.navbar-collapse{align-items:center;flex-basis:100%;flex-grow:1}.navbar-toggler{background-color:transparent;border:1px solid transparent;border-radius:.25rem;font-size:1.25rem;line-height:1;padding:.25rem .75rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{background:50%/100% 100% no-repeat;content:"";display:inline-block;height:1.5em;vertical-align:middle;width:1.5em}.navbar-nav-scroll{max-height:75vh;overflow-y:auto}@media (max-width:1.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{padding-left:0;padding-right:0}}@media (min-width:2px){.navbar-expand-sm{flex-flow:row 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-left:.5rem;padding-right:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{flex-wrap:nowrap}.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}}@media (max-width:7.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{padding-left:0;padding-right:0}}@media (min-width:8px){.navbar-expand-md{flex-flow:row 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-left:.5rem;padding-right:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{flex-wrap:nowrap}.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}}@media (max-width:8.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{padding-left:0;padding-right:0}}@media (min-width:9px){.navbar-expand-lg{flex-flow:row 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-left:.5rem;padding-right:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{flex-wrap:nowrap}.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}}@media (max-width:9.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{padding-left:0;padding-right:0}}@media (min-width:10px){.navbar-expand-xl{flex-flow:row 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-left:.5rem;padding-right:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{flex-wrap:nowrap}.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{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{padding-left:0;padding-right:0}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-left:.5rem;padding-right:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{flex-wrap:nowrap}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand,.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{border-color:rgba(0,0,0,.1);color:rgba(0,0,0,.5)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand,.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:hsla(0,0%,100%,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:hsla(0,0%,100%,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:hsla(0,0%,100%,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{border-color:hsla(0,0%,100%,.1);color:hsla(0,0%,100%,.5)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-dark .navbar-text{color:hsla(0,0%,100%,.5)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{word-wrap:break-word;background-clip:border-box;background-color:#fff;border:1px solid rgba(0,0,0,.125);border-radius:6px;display:flex;flex-direction:column;min-width:0;position:relative}.card>hr{margin-left:0;margin-right:0}.card>.list-group{border-bottom:inherit;border-top:inherit}.card>.list-group:first-child{border-top-left-radius:5px;border-top-right-radius:5px;border-top-width:0}.card>.list-group:last-child{border-bottom-left-radius:5px;border-bottom-right-radius:5px;border-bottom-width:0}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem}.card-subtitle,.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{background-color:#fff;border-bottom:1px solid rgba(0,0,0,.125);margin-bottom:0;padding:.75rem 1.25rem}.card-header:first-child{border-radius:5px 5px 0 0}.card-footer{background-color:#fff;border-top:1px solid rgba(0,0,0,.125);padding:.75rem 1.25rem}.card-footer:last-child{border-radius:0 0 5px 5px}.card-header-tabs{border-bottom:0;margin-bottom:-.75rem}.card-header-pills,.card-header-tabs{margin-left:-.625rem;margin-right:-.625rem}.card-img-overlay{border-radius:5px;bottom:0;left:0;padding:1.25rem;position:absolute;right:0;top:0}.card-img,.card-img-bottom,.card-img-top{flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:5px;border-top-right-radius:5px}.card-img,.card-img-bottom{border-bottom-left-radius:5px;border-bottom-right-radius:5px}.card-deck .card{margin-bottom:15px}@media (min-width:2px){.card-deck{display:flex;flex-flow:row wrap;margin-left:-15px;margin-right:-15px}.card-deck .card{flex:1 0 0%;margin-bottom:0;margin-left:15px;margin-right:15px}}.card-group>.card{margin-bottom:15px}@media (min-width:2px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{border-left:0;margin-left:0}.card-group>.card:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:2px){.card-columns{-moz-column-count:3;column-count:3;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion{overflow-anchor:none}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-left-radius:0;border-bottom-right-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{background-color:#e5e7eb;border-radius:.25rem;display:flex;flex-wrap:wrap;list-style:none;margin-bottom:1rem;padding:.75rem 1rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item:before{color:#4b5563;content:"/";float:left;padding-right:.5rem}.breadcrumb-item+.breadcrumb-item:hover:before{text-decoration:underline;text-decoration:none}.breadcrumb-item.active{color:#4b5563}.pagination{border-radius:.25rem;display:flex;list-style:none;padding-left:0}.page-link{background-color:#fff;border:1px solid #d1d5db;color:#6366f1;display:block;line-height:1.25;margin-left:-1px;padding:.5rem .75rem;position:relative}.page-link:hover{background-color:#e5e7eb;border-color:#d1d5db;color:#4f46e5;text-decoration:none;z-index:2}.page-link:focus{box-shadow:0 0 0 .2rem rgba(64,64,200,.25);outline:0;z-index:3}.page-item:first-child .page-link{border-bottom-left-radius:.25rem;border-top-left-radius:.25rem;margin-left:0}.page-item:last-child .page-link{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.page-item.active .page-link{background-color:#4040c8;border-color:#4040c8;color:#fff;z-index:3}.page-item.disabled .page-link{background-color:#fff;border-color:#d1d5db;color:#4b5563;cursor:auto;pointer-events:none}.pagination-lg .page-link{font-size:1.25rem;line-height:1.5;padding:.75rem 1.5rem}.pagination-lg .page-item:first-child .page-link{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg .page-item:last-child .page-link{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm .page-link{font-size:.875rem;line-height:1.5;padding:.25rem .5rem}.pagination-sm .page-item:first-child .page-link{border-bottom-left-radius:.2rem;border-top-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-bottom-right-radius:.2rem;border-top-right-radius:.2rem}.badge{border-radius:.25rem;display:inline-block;font-size:.875rem;font-weight:600;line-height:1;padding:.25em .4em;text-align:center;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:baseline;white-space:nowrap}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{border-radius:10rem;padding-left:.6em;padding-right:.6em}.badge-primary{background-color:#4040c8;color:#fff}a.badge-primary:focus,a.badge-primary:hover{background-color:#3030a5;color:#fff}a.badge-primary.focus,a.badge-primary:focus{box-shadow:0 0 0 .2rem rgba(64,64,200,.5);outline:0}.badge-secondary{background-color:#4b5563;color:#fff}a.badge-secondary:focus,a.badge-secondary:hover{background-color:#353c46;color:#fff}a.badge-secondary.focus,a.badge-secondary:focus{box-shadow:0 0 0 .2rem rgba(75,85,99,.5);outline:0}.badge-success{background-color:#059669;color:#fff}a.badge-success:focus,a.badge-success:hover{background-color:#036546;color:#fff}a.badge-success.focus,a.badge-success:focus{box-shadow:0 0 0 .2rem rgba(5,150,105,.5);outline:0}.badge-info{background-color:#2563eb;color:#fff}a.badge-info:focus,a.badge-info:hover{background-color:#134cca;color:#fff}a.badge-info.focus,a.badge-info:focus{box-shadow:0 0 0 .2rem rgba(37,99,235,.5);outline:0}.badge-warning{background-color:#d97706;color:#fff}a.badge-warning:focus,a.badge-warning:hover{background-color:#a75c05;color:#fff}a.badge-warning.focus,a.badge-warning:focus{box-shadow:0 0 0 .2rem rgba(217,119,6,.5);outline:0}.badge-danger{background-color:#dc2626;color:#fff}a.badge-danger:focus,a.badge-danger:hover{background-color:#b21d1d;color:#fff}a.badge-danger.focus,a.badge-danger:focus{box-shadow:0 0 0 .2rem rgba(220,38,38,.5);outline:0}.badge-light{background-color:#f3f4f6;color:#111827}a.badge-light:focus,a.badge-light:hover{background-color:#d6d9e0;color:#111827}a.badge-light.focus,a.badge-light:focus{box-shadow:0 0 0 .2rem rgba(243,244,246,.5);outline:0}.badge-dark{background-color:#1f2937;color:#fff}a.badge-dark:focus,a.badge-dark:hover{background-color:#0d1116;color:#fff}a.badge-dark.focus,a.badge-dark:focus{box-shadow:0 0 0 .2rem rgba(31,41,55,.5);outline:0}.jumbotron{background-color:#e5e7eb;border-radius:6px;margin-bottom:2rem;padding:2rem 1rem}@media (min-width:2px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{border-radius:0;padding-left:0;padding-right:0}.alert{border:1px solid transparent;border-radius:.25rem;margin-bottom:1rem;padding:.75rem 1.25rem;position:relative}.alert-heading{color:inherit}.alert-link{font-weight:600}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{color:inherit;padding:.75rem 1.25rem;position:absolute;right:0;top:0;z-index:2}.alert-primary{background-color:#d9d9f4;border-color:#cacaf0;color:#212168}.alert-primary hr{border-top-color:#b6b6ea}.alert-primary .alert-link{color:#151541}.alert-secondary{background-color:#dbdde0;border-color:#cdcfd3;color:#272c33}.alert-secondary hr{border-top-color:#bfc2c7}.alert-secondary .alert-link{color:#111316}.alert-success{background-color:#cdeae1;border-color:#b9e2d5;color:#034e37}.alert-success hr{border-top-color:#a7dbca}.alert-success .alert-link{color:#011d14}.alert-info{background-color:#d3e0fb;border-color:#c2d3f9;color:#13337a}.alert-info hr{border-top-color:#abc2f7}.alert-info .alert-link{color:#0c214e}.alert-warning{background-color:#f7e4cd;border-color:#f4d9b9;color:#713e03}.alert-warning hr{border-top-color:#f1cda3}.alert-warning .alert-link{color:#3f2302}.alert-danger{background-color:#f8d4d4;border-color:#f5c2c2;color:#721414}.alert-danger hr{border-top-color:#f1acac}.alert-danger .alert-link{color:#470c0c}.alert-light{background-color:#fdfdfd;border-color:#fcfcfc;color:#7e7f80}.alert-light hr{border-top-color:#efefef}.alert-light .alert-link{color:#656666}.alert-dark{background-color:#d2d4d7;border-color:#c0c3c7;color:#10151d}.alert-dark hr{border-top-color:#b3b6bb}.alert-dark .alert-link{color:#000}@keyframes progress-bar-stripes{0%{background-position:1rem 0}to{background-position:0 0}}.progress{background-color:#e5e7eb;border-radius:.25rem;font-size:.75rem;height:1rem;line-height:0}.progress,.progress-bar{display:flex;overflow:hidden}.progress-bar{background-color:#4040c8;color:#fff;flex-direction:column;justify-content:center;text-align:center;transition:width .6s ease;white-space:nowrap}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.15) 0,hsla(0,0%,100%,.15) 75%,transparent 0,transparent);background-size:1rem 1rem}.progress-bar-animated{animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.media{align-items:flex-start;display:flex}.media-body{flex:1}.list-group{border-radius:.25rem;display:flex;flex-direction:column;margin-bottom:0;padding-left:0}.list-group-item-action{color:#374151;text-align:inherit;width:100%}.list-group-item-action:focus,.list-group-item-action:hover{background-color:#f3f4f6;color:#374151;text-decoration:none;z-index:1}.list-group-item-action:active{background-color:#e5e7eb;color:#111827}.list-group-item{background-color:#fff;border:1px solid rgba(0,0,0,.125);display:block;padding:.75rem 1.25rem;position:relative}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-left-radius:inherit;border-bottom-right-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{background-color:#fff;color:#4b5563;pointer-events:none}.list-group-item.active{background-color:#4040c8;border-color:#4040c8;color:#fff;z-index:2}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{border-top-width:1px;margin-top:-1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-bottom-left-radius:0;border-top-right-radius:.25rem}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-left-width:0;border-top-width:1px}.list-group-horizontal>.list-group-item+.list-group-item.active{border-left-width:1px;margin-left:-1px}@media (min-width:2px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-bottom-left-radius:0;border-top-right-radius:.25rem}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-left-width:0;border-top-width:1px}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{border-left-width:1px;margin-left:-1px}}@media (min-width:8px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-bottom-left-radius:0;border-top-right-radius:.25rem}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-left-width:0;border-top-width:1px}.list-group-horizontal-md>.list-group-item+.list-group-item.active{border-left-width:1px;margin-left:-1px}}@media (min-width:9px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-bottom-left-radius:0;border-top-right-radius:.25rem}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-left-width:0;border-top-width:1px}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{border-left-width:1px;margin-left:-1px}}@media (min-width:10px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-bottom-left-radius:0;border-top-right-radius:.25rem}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-left-width:0;border-top-width:1px}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{border-left-width:1px;margin-left:-1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{background-color:#cacaf0;color:#212168}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{background-color:#b6b6ea;color:#212168}.list-group-item-primary.list-group-item-action.active{background-color:#212168;border-color:#212168;color:#fff}.list-group-item-secondary{background-color:#cdcfd3;color:#272c33}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{background-color:#bfc2c7;color:#272c33}.list-group-item-secondary.list-group-item-action.active{background-color:#272c33;border-color:#272c33;color:#fff}.list-group-item-success{background-color:#b9e2d5;color:#034e37}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{background-color:#a7dbca;color:#034e37}.list-group-item-success.list-group-item-action.active{background-color:#034e37;border-color:#034e37;color:#fff}.list-group-item-info{background-color:#c2d3f9;color:#13337a}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{background-color:#abc2f7;color:#13337a}.list-group-item-info.list-group-item-action.active{background-color:#13337a;border-color:#13337a;color:#fff}.list-group-item-warning{background-color:#f4d9b9;color:#713e03}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{background-color:#f1cda3;color:#713e03}.list-group-item-warning.list-group-item-action.active{background-color:#713e03;border-color:#713e03;color:#fff}.list-group-item-danger{background-color:#f5c2c2;color:#721414}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{background-color:#f1acac;color:#721414}.list-group-item-danger.list-group-item-action.active{background-color:#721414;border-color:#721414;color:#fff}.list-group-item-light{background-color:#fcfcfc;color:#7e7f80}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{background-color:#efefef;color:#7e7f80}.list-group-item-light.list-group-item-action.active{background-color:#7e7f80;border-color:#7e7f80;color:#fff}.list-group-item-dark{background-color:#c0c3c7;color:#10151d}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{background-color:#b3b6bb;color:#10151d}.list-group-item-dark.list-group-item-action.active{background-color:#10151d;border-color:#10151d;color:#fff}.close{color:#000;float:right;font-size:1.5rem;font-weight:600;line-height:1;opacity:.5;text-shadow:0 1px 0 #fff}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{background-color:transparent;border:0;padding:0}a.close.disabled{pointer-events:none}.toast{background-clip:padding-box;background-color:hsla(0,0%,100%,.85);border:1px solid rgba(0,0,0,.1);border-radius:.25rem;box-shadow:0 .25rem .75rem rgba(0,0,0,.1);flex-basis:350px;font-size:.875rem;max-width:350px;opacity:0}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{align-items:center;background-clip:padding-box;background-color:hsla(0,0%,100%,.85);border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px);color:#4b5563;display:flex;padding:.25rem .75rem}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{display:none;height:100%;left:0;outline:0;overflow:hidden;position:fixed;top:0;width:100%;z-index:1050}.modal-dialog{margin:.5rem;pointer-events:none;position:relative;width:auto}.modal.fade .modal-dialog{transform:translateY(-50px);transition:transform .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{display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{align-items:center;display:flex;min-height:calc(100% - 1rem)}.modal-dialog-centered:before{content:"";display:block;height:calc(100vh - 1rem);height:-moz-min-content;height:min-content}.modal-dialog-centered.modal-dialog-scrollable{flex-direction:column;height:100%;justify-content:center}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable:before{content:none}.modal-content{background-clip:padding-box;background-color:#fff;border:1px solid rgba(0,0,0,.2);border-radius:6px;display:flex;flex-direction:column;outline:0;pointer-events:auto;position:relative;width:100%}.modal-backdrop{background-color:#000;height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:1040}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{align-items:flex-start;border-bottom:1px solid #d1d5db;border-top-left-radius:5px;border-top-right-radius:5px;display:flex;justify-content:space-between;padding:1rem}.modal-header .close{margin:-1rem -1rem -1rem auto;padding:1rem}.modal-title{line-height:1.5;margin-bottom:0}.modal-body{flex:1 1 auto;padding:1rem;position:relative}.modal-footer{align-items:center;border-bottom-left-radius:5px;border-bottom-right-radius:5px;border-top:1px solid #d1d5db;display:flex;flex-wrap:wrap;justify-content:flex-end;padding:.75rem}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{height:50px;overflow:scroll;position:absolute;top:-9999px;width:50px}@media (min-width:2px){.modal-dialog{margin:1.75rem auto;max-width:500px}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered:before{height:calc(100vh - 3.5rem);height:-moz-min-content;height:min-content}.modal-sm{max-width:300px}}@media (min-width:9px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:10px){.modal-xl{max-width:1140px}}.tooltip{word-wrap:break-word;display:block;font-family:Figtree,sans-serif;font-size:.875rem;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.5;margin:0;opacity:0;position:absolute;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;z-index:1070}.tooltip.show{opacity:.9}.tooltip .arrow{display:block;height:.4rem;position:absolute;width:.8rem}.tooltip .arrow:before{border-color:transparent;border-style:solid;content:"";position:absolute}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow:before,.bs-tooltip-top .arrow:before{border-top-color:#000;border-width:.4rem .4rem 0;top:0}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{height:.8rem;left:0;width:.4rem}.bs-tooltip-auto[x-placement^=right] .arrow:before,.bs-tooltip-right .arrow:before{border-right-color:#000;border-width:.4rem .4rem .4rem 0;right:0}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow:before,.bs-tooltip-bottom .arrow:before{border-bottom-color:#000;border-width:0 .4rem .4rem;bottom:0}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{height:.8rem;right:0;width:.4rem}.bs-tooltip-auto[x-placement^=left] .arrow:before,.bs-tooltip-left .arrow:before{border-left-color:#000;border-width:.4rem 0 .4rem .4rem;left:0}.tooltip-inner{background-color:#000;border-radius:.25rem;color:#fff;max-width:200px;padding:.25rem .5rem;text-align:center}.popover{word-wrap:break-word;background-clip:padding-box;background-color:#fff;border:1px solid rgba(0,0,0,.2);border-radius:6px;font-family:Figtree,sans-serif;font-size:.875rem;font-style:normal;font-weight:400;left:0;letter-spacing:normal;line-break:auto;line-height:1.5;max-width:276px;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;top:0;white-space:normal;word-break:normal;word-spacing:normal;z-index:1060}.popover,.popover .arrow{display:block;position:absolute}.popover .arrow{height:.5rem;margin:0 6px;width:1rem}.popover .arrow:after,.popover .arrow:before{border-color:transparent;border-style:solid;content:"";display:block;position:absolute}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=top]>.arrow:before,.bs-popover-top>.arrow:before{border-top-color:rgba(0,0,0,.25);border-width:.5rem .5rem 0;bottom:0}.bs-popover-auto[x-placement^=top]>.arrow:after,.bs-popover-top>.arrow:after{border-top-color:#fff;border-width:.5rem .5rem 0;bottom:1px}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{height:1rem;left:calc(-.5rem - 1px);margin:6px 0;width:.5rem}.bs-popover-auto[x-placement^=right]>.arrow:before,.bs-popover-right>.arrow:before{border-right-color:rgba(0,0,0,.25);border-width:.5rem .5rem .5rem 0;left:0}.bs-popover-auto[x-placement^=right]>.arrow:after,.bs-popover-right>.arrow:after{border-right-color:#fff;border-width:.5rem .5rem .5rem 0;left:1px}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=bottom]>.arrow:before,.bs-popover-bottom>.arrow:before{border-bottom-color:rgba(0,0,0,.25);border-width:0 .5rem .5rem;top:0}.bs-popover-auto[x-placement^=bottom]>.arrow:after,.bs-popover-bottom>.arrow:after{border-bottom-color:#fff;border-width:0 .5rem .5rem;top:1px}.bs-popover-auto[x-placement^=bottom] .popover-header:before,.bs-popover-bottom .popover-header:before{border-bottom:1px solid #f7f7f7;content:"";display:block;left:50%;margin-left:-.5rem;position:absolute;top:0;width:1rem}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{height:1rem;margin:6px 0;right:calc(-.5rem - 1px);width:.5rem}.bs-popover-auto[x-placement^=left]>.arrow:before,.bs-popover-left>.arrow:before{border-left-color:rgba(0,0,0,.25);border-width:.5rem 0 .5rem .5rem;right:0}.bs-popover-auto[x-placement^=left]>.arrow:after,.bs-popover-left>.arrow:after{border-left-color:#fff;border-width:.5rem 0 .5rem .5rem;right:1px}.popover-header{background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:5px;border-top-right-radius:5px;font-size:1rem;margin-bottom:0;padding:.5rem .75rem}.popover-header:empty{display:none}.popover-body{color:#111827;padding:.5rem .75rem}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{overflow:hidden;position:relative;width:100%}.carousel-inner:after{clear:both;content:"";display:block}.carousel-item{backface-visibility:hidden;display:none;float:left;margin-right:-100%;position:relative;transition:transform .6s ease-in-out;width:100%}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transform:none;transition-property:opacity}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{opacity:1;z-index:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{opacity:0;transition:opacity 0s .6s;z-index:0}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{align-items:center;background:none;border:0;bottom:0;color:#fff;display:flex;justify-content:center;opacity:.5;padding:0;position:absolute;text-align:center;top:0;transition:opacity .15s ease;width:15%;z-index:1}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;opacity:.9;outline:0;text-decoration:none}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{background:50%/100% 100% no-repeat;display:inline-block;height:20px;width:20px}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8'%3E%3Cpath d='m5.25 0-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8'%3E%3Cpath d='m2.75 0-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{bottom:0;display:flex;justify-content:center;left:0;list-style:none;margin-left:15%;margin-right:15%;padding-left:0;position:absolute;right:0;z-index:15}.carousel-indicators li{background-clip:padding-box;background-color:#fff;border-bottom:10px solid transparent;border-top:10px solid transparent;box-sizing:content-box;cursor:pointer;flex:0 1 auto;height:3px;margin-left:3px;margin-right:3px;opacity:.5;text-indent:-999px;transition:opacity .6s ease;width:30px}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{bottom:20px;color:#fff;left:15%;padding-bottom:20px;padding-top:20px;position:absolute;right:15%;text-align:center;z-index:10}@keyframes spinner-border{to{transform:rotate(1turn)}}.spinner-border{animation:spinner-border .75s linear infinite;border:.25em solid;border-radius:50%;border-right:.25em solid transparent;display:inline-block;height:2rem;vertical-align:-.125em;width:2rem}.spinner-border-sm{border-width:.2em;height:1rem;width:1rem}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{animation:spinner-grow .75s linear infinite;background-color:currentcolor;border-radius:50%;display:inline-block;height:2rem;opacity:0;vertical-align:-.125em;width:2rem}.spinner-grow-sm{height:1rem;width:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{animation-duration:1.5s}}.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}.bg-primary{background-color:#4040c8!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#3030a5!important}.bg-secondary{background-color:#4b5563!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#353c46!important}.bg-success{background-color:#059669!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#036546!important}.bg-info{background-color:#2563eb!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#134cca!important}.bg-warning{background-color:#d97706!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#a75c05!important}.bg-danger{background-color:#dc2626!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#b21d1d!important}.bg-light{background-color:#f3f4f6!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#d6d9e0!important}.bg-dark{background-color:#1f2937!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#0d1116!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #d1d5db!important}.border-top{border-top:1px solid #d1d5db!important}.border-right{border-right:1px solid #d1d5db!important}.border-bottom{border-bottom:1px solid #d1d5db!important}.border-left{border-left:1px solid #d1d5db!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#4040c8!important}.border-secondary{border-color:#4b5563!important}.border-success{border-color:#059669!important}.border-info{border-color:#2563eb!important}.border-warning{border-color:#d97706!important}.border-danger{border-color:#dc2626!important}.border-light{border-color:#f3f4f6!important}.border-dark{border-color:#1f2937!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important}.rounded-right,.rounded-top{border-top-right-radius:.25rem!important}.rounded-bottom,.rounded-right{border-bottom-right-radius:.25rem!important}.rounded-bottom,.rounded-left{border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important}.rounded-lg{border-radius:6px!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix:after{clear:both;content:"";display:block}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!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}@media (min-width:2px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!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}}@media (min-width:8px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!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}}@media (min-width:9px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!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}}@media (min-width:10px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!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}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!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}}.embed-responsive{display:block;overflow:hidden;padding:0;position:relative;width:100%}.embed-responsive:before{content:"";display:block}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{border:0;bottom:0;height:100%;left:0;position:absolute;top:0;width:100%}.embed-responsive-21by9:before{padding-top:42.85714286%}.embed-responsive-16by9:before{padding-top:56.25%}.embed-responsive-4by3:before{padding-top:75%}.embed-responsive-1by1:before{padding-top:100%}.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-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-fill{flex:1 1 auto!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}.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}.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}@media (min-width:2px){.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-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-sm-fill{flex:1 1 auto!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}.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}.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}}@media (min-width:8px){.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-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-md-fill{flex:1 1 auto!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}.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}.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}}@media (min-width:9px){.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-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-lg-fill{flex:1 1 auto!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}.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}.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}}@media (min-width:10px){.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-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-xl-fill{flex:1 1 auto!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}.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}.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}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:2px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:8px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:9px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:10px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!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}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:sticky!important}.fixed-top{top:0}.fixed-bottom,.fixed-top{left:0;position:fixed;right:0;z-index:1030}.fixed-bottom{bottom:0}@supports (position:sticky){.sticky-top{position:sticky;top:0;z-index:1020}}.sr-only{clip:rect(0,0,0,0);border:0;height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;overflow:visible;position:static;white-space:normal;width:auto}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.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}.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}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:2px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:8px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:9px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:10px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.stretched-link:after{background-color:transparent;bottom:0;content:"";left:0;pointer-events:auto;position:absolute;right:0;top:0;z-index:1}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:2px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:8px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:9px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:10px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:600!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#4040c8!important}a.text-primary:focus,a.text-primary:hover{color:#2a2a92!important}.text-secondary{color:#4b5563!important}a.text-secondary:focus,a.text-secondary:hover{color:#2a3037!important}.text-success{color:#059669!important}a.text-success:focus,a.text-success:hover{color:#034c35!important}.text-info{color:#2563eb!important}a.text-info:focus,a.text-info:hover{color:#1043b3!important}.text-warning{color:#d97706!important}a.text-warning:focus,a.text-warning:hover{color:#8f4e04!important}.text-danger{color:#dc2626!important}a.text-danger:focus,a.text-danger:hover{color:#9c1919!important}.text-light{color:#f3f4f6!important}a.text-light:focus,a.text-light:hover{color:#c7ccd5!important}.text-dark{color:#1f2937!important}a.text-dark:focus,a.text-dark:hover{color:#030506!important}.text-body{color:#111827!important}.text-muted{color:#6b7280!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:hsla(0,0%,100%,.5)!important}.text-hide{background-color:transparent;border:0;color:transparent;font:0/0 a;text-shadow:none}.text-decoration-none{text-decoration:none!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,:after,:before{box-shadow:none!important;text-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]:after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #6b7280}blockquote,img,pre,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}.container,body{min-width:9px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #d1d5db!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#e5e7eb}.table .thead-dark th{border-color:#e5e7eb;color:inherit}}.vjs-tree{color:#bfc7d5!important;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.vjs-tree .vjs-tree__content{border-left:1px dotted hsla(0,0%,80%,.28)!important}.vjs-tree .vjs-tree__node{cursor:pointer}.vjs-tree .vjs-tree__node:hover{color:#20a0ff}.vjs-tree .vjs-checkbox{left:-30px;position:absolute}.vjs-tree .vjs-value__boolean,.vjs-tree .vjs-value__null,.vjs-tree .vjs-value__number{color:#a291f5!important}.vjs-tree .vjs-value__string{color:#c3e88d!important}.vjs-tree .vjs-key{color:#c3cbd3!important}.hljs-addition,.hljs-attr,.hljs-keyword,.hljs-selector-tag{color:#13ce66}.hljs-bullet,.hljs-meta,.hljs-name,.hljs-string,.hljs-symbol,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable{color:#c3e88d}.hljs-comment,.hljs-deletion,.hljs-quote{color:#bfcbd9}.hljs-literal,.hljs-number,.hljs-title{color:#a291f5!important}body{padding-bottom:20px}.container{max-width:1440px}html{min-width:1140px}[v-cloak]{display:none}svg.icon{height:1rem;width:1rem}.header{border-bottom:1px solid #e5e7eb}.header .logo{color:#374151;text-decoration:none}.header .logo svg{height:1.7rem;width:1.7rem}.sidebar .nav-item a{border-radius:6px;color:#4b5563;margin-bottom:4px;padding:.5rem .75rem}.sidebar .nav-item a svg{fill:#9ca3af;height:1.25rem;margin-right:15px;width:1.25rem}.sidebar .nav-item a.active,.sidebar .nav-item a:hover{background-color:#e5e7eb;color:#4040c8}.sidebar .nav-item a.active svg{fill:#4040c8}.card{border:none;box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1)}.card .bottom-radius{border-bottom-left-radius:6px;border-bottom-right-radius:6px}.card .card-header{background-color:#fff;border-bottom:none;min-height:60px;padding-bottom:.7rem;padding-top:.7rem}.card .card-header .btn-group .btn{padding:.2rem .5rem}.card .card-header .form-control-with-icon{position:relative}.card .card-header .form-control-with-icon .icon-wrapper{align-items:center;bottom:0;display:flex;justify-content:center;left:.75rem;position:absolute;top:0}.card .card-header .form-control-with-icon .icon-wrapper .icon{fill:#6b7280}.card .card-header .form-control-with-icon .form-control{border-radius:9999px;font-size:.875rem;padding-left:2.25rem}.card .table td,.card .table th{padding:.75rem 1.25rem}.card .table th{background-color:#f3f4f6;border-bottom:0;font-size:.875rem;padding:.5rem 1.25rem}.card .table:not(.table-borderless) td{border-top:1px solid #e5e7eb}.card .table.penultimate-column-right td:nth-last-child(2),.card .table.penultimate-column-right th:nth-last-child(2){text-align:right}.card .table td.table-fit,.card .table th.table-fit{white-space:nowrap;width:1%}.fill-text-color{fill:#111827}.fill-danger{fill:#dc2626}.fill-warning{fill:#d97706}.fill-info{fill:#2563eb}.fill-success{fill:#059669}.fill-primary{fill:#4040c8}button:hover .fill-primary{fill:#fff}.btn-outline-primary.active .fill-primary{fill:#f3f4f6}.btn-outline-primary:not(:disabled):not(.disabled).active:focus{box-shadow:none!important}.btn-muted{background:#e5e7eb;color:#4b5563}.btn-muted:focus,.btn-muted:hover{background:#d1d5db;color:#111827}.btn-muted.active{background:#4040c8;color:#fff}.badge-secondary{background:#e5e7eb;color:#4b5563}.badge-success{background:#d1fae5;color:#059669}.badge-info{background:#dbeafe;color:#2563eb}.badge-warning{background:#fef3c7;color:#d97706}.badge-danger{background:#fee2e2;color:#dc2626}.control-action svg{fill:#d1d5db;height:1.2rem;width:1.2rem}.control-action svg:hover{fill:#4f46e5}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.spin{animation:spin 2s linear infinite}.card .nav-pills{background:#fff}.card .nav-pills .nav-link{border-radius:0;color:#4b5563;font-size:.9rem;padding:.75rem 1.25rem}.card .nav-pills .nav-link:focus,.card .nav-pills .nav-link:hover{color:#1f2937}.card .nav-pills .nav-link.active{background:none;border-bottom:2px solid #4f46e5;color:#4f46e5}.list-enter-active:not(.dontanimate){transition:background 1s linear}.list-enter:not(.dontanimate),.list-leave-to:not(.dontanimate){background:#eef2ff}.code-bg .list-enter:not(.dontanimate),.code-bg .list-leave-to:not(.dontanimate){background:#4b5563}#indexScreen td{vertical-align:middle!important}.card-bg-secondary{background:#f3f4f6}.code-bg{background:#292d3e}.disabled-watcher{background:#dc2626;color:#fff;padding:.75rem}.copy-to-clipboard{--tw-text-opacity:1;color:rgb(231 232 242/var(--tw-text-opacity));opacity:.7;outline:2px solid transparent;outline-offset:2px;position:absolute;right:0;top:0;z-index:10} diff --git a/public/vendor/telescope/app.js b/public/vendor/telescope/app.js new file mode 100644 index 000000000..378d6cf43 --- /dev/null +++ b/public/vendor/telescope/app.js @@ -0,0 +1,2 @@ +/*! For license information please see app.js.LICENSE.txt */ +(()=>{var t,e={2465:(t,e,n)=>{"use strict";var o=Object.freeze({}),p=Array.isArray;function M(t){return null==t}function b(t){return null!=t}function c(t){return!0===t}function r(t){return"string"==typeof t||"number"==typeof t||"symbol"==typeof t||"boolean"==typeof t}function z(t){return"function"==typeof t}function a(t){return null!==t&&"object"==typeof t}var i=Object.prototype.toString;function O(t){return"[object Object]"===i.call(t)}function s(t){return"[object RegExp]"===i.call(t)}function A(t){var e=parseFloat(String(t));return e>=0&&Math.floor(e)===e&&isFinite(t)}function u(t){return b(t)&&"function"==typeof t.then&&"function"==typeof t.catch}function l(t){return null==t?"":Array.isArray(t)||O(t)&&t.toString===i?JSON.stringify(t,null,2):String(t)}function d(t){var e=parseFloat(t);return isNaN(e)?t:e}function f(t,e){for(var n=Object.create(null),o=t.split(","),p=0;p-1)return t.splice(o,1)}}var v=Object.prototype.hasOwnProperty;function R(t,e){return v.call(t,e)}function m(t){var e=Object.create(null);return function(n){return e[n]||(e[n]=t(n))}}var g=/-(\w)/g,L=m((function(t){return t.replace(g,(function(t,e){return e?e.toUpperCase():""}))})),y=m((function(t){return t.charAt(0).toUpperCase()+t.slice(1)})),_=/\B([A-Z])/g,N=m((function(t){return t.replace(_,"-$1").toLowerCase()}));var E=Function.prototype.bind?function(t,e){return t.bind(e)}:function(t,e){function n(n){var o=arguments.length;return o?o>1?t.apply(e,arguments):t.call(e,n):t.call(e)}return n._length=t.length,n};function T(t,e){e=e||0;for(var n=t.length-e,o=new Array(n);n--;)o[n]=t[n+e];return o}function B(t,e){for(var n in e)t[n]=e[n];return t}function C(t){for(var e={},n=0;n0,tt=Q&&Q.indexOf("edge/")>0;Q&&Q.indexOf("android");var et=Q&&/iphone|ipad|ipod|ios/.test(Q);Q&&/chrome\/\d+/.test(Q),Q&&/phantomjs/.test(Q);var nt,ot=Q&&Q.match(/firefox\/(\d+)/),pt={}.watch,Mt=!1;if(K)try{var bt={};Object.defineProperty(bt,"passive",{get:function(){Mt=!0}}),window.addEventListener("test-passive",null,bt)}catch(t){}var ct=function(){return void 0===nt&&(nt=!K&&void 0!==n.g&&(n.g.process&&"server"===n.g.process.env.VUE_ENV)),nt},rt=K&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__;function zt(t){return"function"==typeof t&&/native code/.test(t.toString())}var at,it="undefined"!=typeof Symbol&&zt(Symbol)&&"undefined"!=typeof Reflect&&zt(Reflect.ownKeys);at="undefined"!=typeof Set&&zt(Set)?Set:function(){function t(){this.set=Object.create(null)}return t.prototype.has=function(t){return!0===this.set[t]},t.prototype.add=function(t){this.set[t]=!0},t.prototype.clear=function(){this.set=Object.create(null)},t}();var Ot=null;function st(t){void 0===t&&(t=null),t||Ot&&Ot._scope.off(),Ot=t,t&&t._scope.on()}var At=function(){function t(t,e,n,o,p,M,b,c){this.tag=t,this.data=e,this.children=n,this.text=o,this.elm=p,this.ns=void 0,this.context=M,this.fnContext=void 0,this.fnOptions=void 0,this.fnScopeId=void 0,this.key=e&&e.key,this.componentOptions=b,this.componentInstance=void 0,this.parent=void 0,this.raw=!1,this.isStatic=!1,this.isRootInsert=!0,this.isComment=!1,this.isCloned=!1,this.isOnce=!1,this.asyncFactory=c,this.asyncMeta=void 0,this.isAsyncPlaceholder=!1}return Object.defineProperty(t.prototype,"child",{get:function(){return this.componentInstance},enumerable:!1,configurable:!0}),t}(),ut=function(t){void 0===t&&(t="");var e=new At;return e.text=t,e.isComment=!0,e};function lt(t){return new At(void 0,void 0,void 0,String(t))}function dt(t){var e=new At(t.tag,t.data,t.children&&t.children.slice(),t.text,t.elm,t.context,t.componentOptions,t.asyncFactory);return e.ns=t.ns,e.isStatic=t.isStatic,e.key=t.key,e.isComment=t.isComment,e.fnContext=t.fnContext,e.fnOptions=t.fnOptions,e.fnScopeId=t.fnScopeId,e.asyncMeta=t.asyncMeta,e.isCloned=!0,e}var ft=0,qt=[],ht=function(){for(var t=0;t0&&(Vt((o=Kt(o,"".concat(e||"","_").concat(n)))[0])&&Vt(a)&&(i[z]=lt(a.text+o[0].text),o.shift()),i.push.apply(i,o)):r(o)?Vt(a)?i[z]=lt(a.text+o):""!==o&&i.push(lt(o)):Vt(o)&&Vt(a)?i[z]=lt(a.text+o.text):(c(t._isVList)&&b(o.tag)&&M(o.key)&&b(e)&&(o.key="__vlist".concat(e,"_").concat(n,"__")),i.push(o)));return i}var Qt=1,Jt=2;function Zt(t,e,n,o,M,i){return(p(n)||r(n))&&(M=o,o=n,n=void 0),c(i)&&(M=Jt),function(t,e,n,o,M){if(b(n)&&b(n.__ob__))return ut();b(n)&&b(n.is)&&(e=n.is);if(!e)return ut();0;p(o)&&z(o[0])&&((n=n||{}).scopedSlots={default:o[0]},o.length=0);M===Jt?o=$t(o):M===Qt&&(o=function(t){for(var e=0;e0,c=e?!!e.$stable:!b,r=e&&e.$key;if(e){if(e._normalized)return e._normalized;if(c&&p&&p!==o&&r===p.$key&&!b&&!p.$hasNormal)return p;for(var z in M={},e)e[z]&&"$"!==z[0]&&(M[z]=he(t,n,z,e[z]))}else M={};for(var a in n)a in M||(M[a]=We(n,a));return e&&Object.isExtensible(e)&&(e._normalized=M),Y(M,"$stable",c),Y(M,"$key",r),Y(M,"$hasNormal",b),M}function he(t,e,n,o){var M=function(){var e=Ot;st(t);var n=arguments.length?o.apply(null,arguments):o({}),M=(n=n&&"object"==typeof n&&!p(n)?[n]:$t(n))&&n[0];return st(e),n&&(!M||1===n.length&&M.isComment&&!fe(M))?void 0:n};return o.proxy&&Object.defineProperty(e,n,{get:M,enumerable:!0,configurable:!0}),M}function We(t,e){return function(){return t[e]}}function ve(t){return{get attrs(){if(!t._attrsProxy){var e=t._attrsProxy={};Y(e,"_v_attr_proxy",!0),Re(e,t.$attrs,o,t,"$attrs")}return t._attrsProxy},get listeners(){t._listenersProxy||Re(t._listenersProxy={},t.$listeners,o,t,"$listeners");return t._listenersProxy},get slots(){return function(t){t._slotsProxy||ge(t._slotsProxy={},t.$scopedSlots);return t._slotsProxy}(t)},emit:E(t.$emit,t),expose:function(e){e&&Object.keys(e).forEach((function(n){return Ut(t,e,n)}))}}}function Re(t,e,n,o,p){var M=!1;for(var b in e)b in t?e[b]!==n[b]&&(M=!0):(M=!0,me(t,b,o,p));for(var b in t)b in e||(M=!0,delete t[b]);return M}function me(t,e,n,o){Object.defineProperty(t,e,{enumerable:!0,configurable:!0,get:function(){return n[o][e]}})}function ge(t,e){for(var n in e)t[n]=e[n];for(var n in t)n in e||delete t[n]}var Le,ye=null;function _e(t,e){return(t.__esModule||it&&"Module"===t[Symbol.toStringTag])&&(t=t.default),a(t)?e.extend(t):t}function Ne(t){if(p(t))for(var e=0;edocument.createEvent("Event").timeStamp&&(Ye=function(){return $e.now()})}var Ve=function(t,e){if(t.post){if(!e.post)return 1}else if(e.post)return-1;return t.id-e.id};function Ke(){var t,e;for(Ge=Ye(),Fe=!0,De.sort(Ve),He=0;HeHe&&De[n].id>t.id;)n--;De.splice(n+1,0,t)}else De.push(t);je||(je=!0,ln(Ke))}}var Je="watcher";"".concat(Je," callback"),"".concat(Je," getter"),"".concat(Je," cleanup");var Ze;var tn=function(){function t(t){void 0===t&&(t=!1),this.detached=t,this.active=!0,this.effects=[],this.cleanups=[],this.parent=Ze,!t&&Ze&&(this.index=(Ze.scopes||(Ze.scopes=[])).push(this)-1)}return t.prototype.run=function(t){if(this.active){var e=Ze;try{return Ze=this,t()}finally{Ze=e}}else 0},t.prototype.on=function(){Ze=this},t.prototype.off=function(){Ze=this.parent},t.prototype.stop=function(t){if(this.active){var e=void 0,n=void 0;for(e=0,n=this.effects.length;e-1)if(M&&!R(p,"default"))b=!1;else if(""===b||b===N(t)){var r=eo(String,p.type);(r<0||c-1:"string"==typeof t?t.split(",").indexOf(e)>-1:!!s(t)&&t.test(e)}function bo(t,e){var n=t.cache,o=t.keys,p=t._vnode;for(var M in n){var b=n[M];if(b){var c=b.name;c&&!e(c)&&co(n,M,o,p)}}}function co(t,e,n,o){var p=t[e];!p||o&&p.tag===o.tag||p.componentInstance.$destroy(),t[e]=null,W(n,e)}!function(t){t.prototype._init=function(t){var e=this;e._uid=Bn++,e._isVue=!0,e.__v_skip=!0,e._scope=new tn(!0),e._scope._vm=!0,t&&t._isComponent?function(t,e){var n=t.$options=Object.create(t.constructor.options),o=e._parentVnode;n.parent=e.parent,n._parentVnode=o;var p=o.componentOptions;n.propsData=p.propsData,n._parentListeners=p.listeners,n._renderChildren=p.children,n._componentTag=p.tag,e.render&&(n.render=e.render,n.staticRenderFns=e.staticRenderFns)}(e,t):e.$options=Vn(Cn(e.constructor),t||{},e),e._renderProxy=e,e._self=e,function(t){var e=t.$options,n=e.parent;if(n&&!e.abstract){for(;n.$options.abstract&&n.$parent;)n=n.$parent;n.$children.push(t)}t.$parent=n,t.$root=n?n.$root:t,t.$children=[],t.$refs={},t._provided=n?n._provided:Object.create(null),t._watcher=null,t._inactive=null,t._directInactive=!1,t._isMounted=!1,t._isDestroyed=!1,t._isBeingDestroyed=!1}(e),function(t){t._events=Object.create(null),t._hasHookEvent=!1;var e=t.$options._parentListeners;e&&Ce(t,e)}(e),function(t){t._vnode=null,t._staticTrees=null;var e=t.$options,n=t.$vnode=e._parentVnode,p=n&&n.context;t.$slots=le(e._renderChildren,p),t.$scopedSlots=n?qe(t.$parent,n.data.scopedSlots,t.$slots):o,t._c=function(e,n,o,p){return Zt(t,e,n,o,p,!1)},t.$createElement=function(e,n,o,p){return Zt(t,e,n,o,p,!0)};var M=n&&n.data;wt(t,"$attrs",M&&M.attrs||o,null,!0),wt(t,"$listeners",e._parentListeners||o,null,!0)}(e),Ie(e,"beforeCreate",void 0,!1),function(t){var e=Tn(t.$options.inject,t);e&&(Et(!1),Object.keys(e).forEach((function(n){wt(t,n,e[n])})),Et(!0))}(e),gn(e),function(t){var e=t.$options.provide;if(e){var n=z(e)?e.call(t):e;if(!a(n))return;for(var o=en(t),p=it?Reflect.ownKeys(n):Object.keys(n),M=0;M1?T(n):n;for(var o=T(arguments,1),p='event handler for "'.concat(t,'"'),M=0,b=n.length;MparseInt(this.max)&&co(e,n[0],n,this._vnode),this.vnodeToCache=null}}},created:function(){this.cache=Object.create(null),this.keys=[]},destroyed:function(){for(var t in this.cache)co(this.cache,t,this.keys)},mounted:function(){var t=this;this.cacheVNode(),this.$watch("include",(function(e){bo(t,(function(t){return Mo(e,t)}))})),this.$watch("exclude",(function(e){bo(t,(function(t){return!Mo(e,t)}))}))},updated:function(){this.cacheVNode()},render:function(){var t=this.$slots.default,e=Ne(t),n=e&&e.componentOptions;if(n){var o=po(n),p=this.include,M=this.exclude;if(p&&(!o||!Mo(p,o))||M&&o&&Mo(M,o))return e;var b=this.cache,c=this.keys,r=null==e.key?n.Ctor.cid+(n.tag?"::".concat(n.tag):""):e.key;b[r]?(e.componentInstance=b[r].componentInstance,W(c,r),c.push(r)):(this.vnodeToCache=e,this.keyToCache=r),e.data.keepAlive=!0}return e||t&&t[0]}},ao={KeepAlive:zo};!function(t){var e={get:function(){return F}};Object.defineProperty(t,"config",e),t.util={warn:Un,extend:B,mergeOptions:Vn,defineReactive:wt},t.set=St,t.delete=Xt,t.nextTick=ln,t.observable=function(t){return Ct(t),t},t.options=Object.create(null),U.forEach((function(e){t.options[e+"s"]=Object.create(null)})),t.options._base=t,B(t.options.components,ao),function(t){t.use=function(t){var e=this._installedPlugins||(this._installedPlugins=[]);if(e.indexOf(t)>-1)return this;var n=T(arguments,1);return n.unshift(this),z(t.install)?t.install.apply(t,n):z(t)&&t.apply(null,n),e.push(t),this}}(t),function(t){t.mixin=function(t){return this.options=Vn(this.options,t),this}}(t),oo(t),function(t){U.forEach((function(e){t[e]=function(t,n){return n?("component"===e&&O(n)&&(n.name=n.name||t,n=this.options._base.extend(n)),"directive"===e&&z(n)&&(n={bind:n,update:n}),this.options[e+"s"][t]=n,n):this.options[e+"s"][t]}}))}(t)}(no),Object.defineProperty(no.prototype,"$isServer",{get:ct}),Object.defineProperty(no.prototype,"$ssrContext",{get:function(){return this.$vnode&&this.$vnode.ssrContext}}),Object.defineProperty(no,"FunctionalRenderContext",{value:wn}),no.version="2.7.14";var io=f("style,class"),Oo=f("input,textarea,option,select,progress"),so=function(t,e,n){return"value"===n&&Oo(t)&&"button"!==e||"selected"===n&&"option"===t||"checked"===n&&"input"===t||"muted"===n&&"video"===t},Ao=f("contenteditable,draggable,spellcheck"),uo=f("events,caret,typing,plaintext-only"),lo=function(t,e){return vo(e)||"false"===e?"false":"contenteditable"===t&&uo(e)?e:"true"},fo=f("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,truespeed,typemustmatch,visible"),qo="http://www.w3.org/1999/xlink",ho=function(t){return":"===t.charAt(5)&&"xlink"===t.slice(0,5)},Wo=function(t){return ho(t)?t.slice(6,t.length):""},vo=function(t){return null==t||!1===t};function Ro(t){for(var e=t.data,n=t,o=t;b(o.componentInstance);)(o=o.componentInstance._vnode)&&o.data&&(e=mo(o.data,e));for(;b(n=n.parent);)n&&n.data&&(e=mo(e,n.data));return function(t,e){if(b(t)||b(e))return go(t,Lo(e));return""}(e.staticClass,e.class)}function mo(t,e){return{staticClass:go(t.staticClass,e.staticClass),class:b(t.class)?[t.class,e.class]:e.class}}function go(t,e){return t?e?t+" "+e:t:e||""}function Lo(t){return Array.isArray(t)?function(t){for(var e,n="",o=0,p=t.length;o-1?Jo(t,e,n):fo(e)?vo(n)?t.removeAttribute(e):(n="allowfullscreen"===e&&"EMBED"===t.tagName?"true":e,t.setAttribute(e,n)):Ao(e)?t.setAttribute(e,lo(e,n)):ho(e)?vo(n)?t.removeAttributeNS(qo,Wo(e)):t.setAttributeNS(qo,e,n):Jo(t,e,n)}function Jo(t,e,n){if(vo(n))t.removeAttribute(e);else{if(J&&!Z&&"TEXTAREA"===t.tagName&&"placeholder"===e&&""!==n&&!t.__ieph){var o=function(e){e.stopImmediatePropagation(),t.removeEventListener("input",o)};t.addEventListener("input",o),t.__ieph=!0}t.setAttribute(e,n)}}var Zo={create:Ko,update:Ko};function tp(t,e){var n=e.elm,o=e.data,p=t.data;if(!(M(o.staticClass)&&M(o.class)&&(M(p)||M(p.staticClass)&&M(p.class)))){var c=Ro(e),r=n._transitionClasses;b(r)&&(c=go(c,Lo(r))),c!==n._prevClass&&(n.setAttribute("class",c),n._prevClass=c)}}var ep,np,op,pp,Mp,bp,cp={create:tp,update:tp},rp=/[\w).+\-_$\]]/;function zp(t){var e,n,o,p,M,b=!1,c=!1,r=!1,z=!1,a=0,i=0,O=0,s=0;for(o=0;o=0&&" "===(u=t.charAt(A));A--);u&&rp.test(u)||(z=!0)}}else void 0===p?(s=o+1,p=t.slice(0,o).trim()):l();function l(){(M||(M=[])).push(t.slice(s,o).trim()),s=o+1}if(void 0===p?p=t.slice(0,o).trim():0!==s&&l(),M)for(o=0;o-1?{exp:t.slice(0,pp),key:'"'+t.slice(pp+1)+'"'}:{exp:t,key:null};np=t,pp=Mp=bp=0;for(;!Lp();)yp(op=gp())?Np(op):91===op&&_p(op);return{exp:t.slice(0,Mp),key:t.slice(Mp+1,bp)}}(t);return null===n.key?"".concat(t,"=").concat(e):"$set(".concat(n.exp,", ").concat(n.key,", ").concat(e,")")}function gp(){return np.charCodeAt(++pp)}function Lp(){return pp>=ep}function yp(t){return 34===t||39===t}function _p(t){var e=1;for(Mp=pp;!Lp();)if(yp(t=gp()))Np(t);else if(91===t&&e++,93===t&&e--,0===e){bp=pp;break}}function Np(t){for(var e=t;!Lp()&&(t=gp())!==e;);}var Ep,Tp="__r",Bp="__c";function Cp(t,e,n){var o=Ep;return function p(){null!==e.apply(null,arguments)&&Xp(t,p,n,o)}}var wp=cn&&!(ot&&Number(ot[1])<=53);function Sp(t,e,n,o){if(wp){var p=Ge,M=e;e=M._wrapper=function(t){if(t.target===t.currentTarget||t.timeStamp>=p||t.timeStamp<=0||t.target.ownerDocument!==document)return M.apply(this,arguments)}}Ep.addEventListener(t,e,Mt?{capture:n,passive:o}:n)}function Xp(t,e,n,o){(o||Ep).removeEventListener(t,e._wrapper||e,n)}function xp(t,e){if(!M(t.data.on)||!M(e.data.on)){var n=e.data.on||{},o=t.data.on||{};Ep=e.elm||t.elm,function(t){if(b(t[Tp])){var e=J?"change":"input";t[e]=[].concat(t[Tp],t[e]||[]),delete t[Tp]}b(t[Bp])&&(t.change=[].concat(t[Bp],t.change||[]),delete t[Bp])}(n),Ht(n,o,Sp,Xp,Cp,e.context),Ep=void 0}}var kp,Ip={create:xp,update:xp,destroy:function(t){return xp(t,Io)}};function Dp(t,e){if(!M(t.data.domProps)||!M(e.data.domProps)){var n,o,p=e.elm,r=t.data.domProps||{},z=e.data.domProps||{};for(n in(b(z.__ob__)||c(z._v_attr_proxy))&&(z=e.data.domProps=B({},z)),r)n in z||(p[n]="");for(n in z){if(o=z[n],"textContent"===n||"innerHTML"===n){if(e.children&&(e.children.length=0),o===r[n])continue;1===p.childNodes.length&&p.removeChild(p.childNodes[0])}if("value"===n&&"PROGRESS"!==p.tagName){p._value=o;var a=M(o)?"":String(o);Pp(p,a)&&(p.value=a)}else if("innerHTML"===n&&No(p.tagName)&&M(p.innerHTML)){(kp=kp||document.createElement("div")).innerHTML="".concat(o,"");for(var i=kp.firstChild;p.firstChild;)p.removeChild(p.firstChild);for(;i.firstChild;)p.appendChild(i.firstChild)}else if(o!==r[n])try{p[n]=o}catch(t){}}}}function Pp(t,e){return!t.composing&&("OPTION"===t.tagName||function(t,e){var n=!0;try{n=document.activeElement!==t}catch(t){}return n&&t.value!==e}(t,e)||function(t,e){var n=t.value,o=t._vModifiers;if(b(o)){if(o.number)return d(n)!==d(e);if(o.trim)return n.trim()!==e.trim()}return n!==e}(t,e))}var Up={create:Dp,update:Dp},jp=m((function(t){var e={},n=/:(.+)/;return t.split(/;(?![^(]*\))/g).forEach((function(t){if(t){var o=t.split(n);o.length>1&&(e[o[0].trim()]=o[1].trim())}})),e}));function Fp(t){var e=Hp(t.style);return t.staticStyle?B(t.staticStyle,e):e}function Hp(t){return Array.isArray(t)?C(t):"string"==typeof t?jp(t):t}var Gp,Yp=/^--/,$p=/\s*!important$/,Vp=function(t,e,n){if(Yp.test(e))t.style.setProperty(e,n);else if($p.test(n))t.style.setProperty(N(e),n.replace($p,""),"important");else{var o=Qp(e);if(Array.isArray(n))for(var p=0,M=n.length;p-1?e.split(tM).forEach((function(e){return t.classList.add(e)})):t.classList.add(e);else{var n=" ".concat(t.getAttribute("class")||""," ");n.indexOf(" "+e+" ")<0&&t.setAttribute("class",(n+e).trim())}}function nM(t,e){if(e&&(e=e.trim()))if(t.classList)e.indexOf(" ")>-1?e.split(tM).forEach((function(e){return t.classList.remove(e)})):t.classList.remove(e),t.classList.length||t.removeAttribute("class");else{for(var n=" ".concat(t.getAttribute("class")||""," "),o=" "+e+" ";n.indexOf(o)>=0;)n=n.replace(o," ");(n=n.trim())?t.setAttribute("class",n):t.removeAttribute("class")}}function oM(t){if(t){if("object"==typeof t){var e={};return!1!==t.css&&B(e,pM(t.name||"v")),B(e,t),e}return"string"==typeof t?pM(t):void 0}}var pM=m((function(t){return{enterClass:"".concat(t,"-enter"),enterToClass:"".concat(t,"-enter-to"),enterActiveClass:"".concat(t,"-enter-active"),leaveClass:"".concat(t,"-leave"),leaveToClass:"".concat(t,"-leave-to"),leaveActiveClass:"".concat(t,"-leave-active")}})),MM=K&&!Z,bM="transition",cM="animation",rM="transition",zM="transitionend",aM="animation",iM="animationend";MM&&(void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend&&(rM="WebkitTransition",zM="webkitTransitionEnd"),void 0===window.onanimationend&&void 0!==window.onwebkitanimationend&&(aM="WebkitAnimation",iM="webkitAnimationEnd"));var OM=K?window.requestAnimationFrame?window.requestAnimationFrame.bind(window):setTimeout:function(t){return t()};function sM(t){OM((function(){OM(t)}))}function AM(t,e){var n=t._transitionClasses||(t._transitionClasses=[]);n.indexOf(e)<0&&(n.push(e),eM(t,e))}function uM(t,e){t._transitionClasses&&W(t._transitionClasses,e),nM(t,e)}function lM(t,e,n){var o=fM(t,e),p=o.type,M=o.timeout,b=o.propCount;if(!p)return n();var c=p===bM?zM:iM,r=0,z=function(){t.removeEventListener(c,a),n()},a=function(e){e.target===t&&++r>=b&&z()};setTimeout((function(){r0&&(n=bM,a=b,i=M.length):e===cM?z>0&&(n=cM,a=z,i=r.length):i=(n=(a=Math.max(b,z))>0?b>z?bM:cM:null)?n===bM?M.length:r.length:0,{type:n,timeout:a,propCount:i,hasTransform:n===bM&&dM.test(o[rM+"Property"])}}function qM(t,e){for(;t.length1}function gM(t,e){!0!==e.data.show&&WM(e)}var LM=function(t){var e,n,o={},z=t.modules,a=t.nodeOps;for(e=0;eA?h(t,M(n[d+1])?null:n[d+1].elm,n,s,d,o):s>d&&v(e,i,A)}(i,u,d,n,z):b(d)?(b(t.text)&&a.setTextContent(i,""),h(i,null,d,0,d.length-1,n)):b(u)?v(u,0,u.length-1):b(t.text)&&a.setTextContent(i,""):t.text!==e.text&&a.setTextContent(i,e.text),b(A)&&b(s=A.hook)&&b(s=s.postpatch)&&s(t,e)}}}function L(t,e,n){if(c(n)&&b(t.parent))t.parent.data.pendingInsert=e;else for(var o=0;o-1,b.selected!==M&&(b.selected=M);else if(x(TM(b),o))return void(t.selectedIndex!==c&&(t.selectedIndex=c));p||(t.selectedIndex=-1)}}function EM(t,e){return e.every((function(e){return!x(e,t)}))}function TM(t){return"_value"in t?t._value:t.value}function BM(t){t.target.composing=!0}function CM(t){t.target.composing&&(t.target.composing=!1,wM(t.target,"input"))}function wM(t,e){var n=document.createEvent("HTMLEvents");n.initEvent(e,!0,!0),t.dispatchEvent(n)}function SM(t){return!t.componentInstance||t.data&&t.data.transition?t:SM(t.componentInstance._vnode)}var XM={bind:function(t,e,n){var o=e.value,p=(n=SM(n)).data&&n.data.transition,M=t.__vOriginalDisplay="none"===t.style.display?"":t.style.display;o&&p?(n.data.show=!0,WM(n,(function(){t.style.display=M}))):t.style.display=o?M:"none"},update:function(t,e,n){var o=e.value;!o!=!e.oldValue&&((n=SM(n)).data&&n.data.transition?(n.data.show=!0,o?WM(n,(function(){t.style.display=t.__vOriginalDisplay})):vM(n,(function(){t.style.display="none"}))):t.style.display=o?t.__vOriginalDisplay:"none")},unbind:function(t,e,n,o,p){p||(t.style.display=t.__vOriginalDisplay)}},xM={model:yM,show:XM},kM={name:String,appear:Boolean,css:Boolean,mode:String,type:String,enterClass:String,leaveClass:String,enterToClass:String,leaveToClass:String,enterActiveClass:String,leaveActiveClass:String,appearClass:String,appearActiveClass:String,appearToClass:String,duration:[Number,String,Object]};function IM(t){var e=t&&t.componentOptions;return e&&e.Ctor.options.abstract?IM(Ne(e.children)):t}function DM(t){var e={},n=t.$options;for(var o in n.propsData)e[o]=t[o];var p=n._parentListeners;for(var o in p)e[L(o)]=p[o];return e}function PM(t,e){if(/\d-keep-alive$/.test(e.tag))return t("keep-alive",{props:e.componentOptions.propsData})}var UM=function(t){return t.tag||fe(t)},jM=function(t){return"show"===t.name},FM={name:"transition",props:kM,abstract:!0,render:function(t){var e=this,n=this.$slots.default;if(n&&(n=n.filter(UM)).length){0;var o=this.mode;0;var p=n[0];if(function(t){for(;t=t.parent;)if(t.data.transition)return!0}(this.$vnode))return p;var M=IM(p);if(!M)return p;if(this._leaving)return PM(t,p);var b="__transition-".concat(this._uid,"-");M.key=null==M.key?M.isComment?b+"comment":b+M.tag:r(M.key)?0===String(M.key).indexOf(b)?M.key:b+M.key:M.key;var c=(M.data||(M.data={})).transition=DM(this),z=this._vnode,a=IM(z);if(M.data.directives&&M.data.directives.some(jM)&&(M.data.show=!0),a&&a.data&&!function(t,e){return e.key===t.key&&e.tag===t.tag}(M,a)&&!fe(a)&&(!a.componentInstance||!a.componentInstance._vnode.isComment)){var i=a.data.transition=B({},c);if("out-in"===o)return this._leaving=!0,Gt(i,"afterLeave",(function(){e._leaving=!1,e.$forceUpdate()})),PM(t,p);if("in-out"===o){if(fe(M))return z;var O,s=function(){O()};Gt(c,"afterEnter",s),Gt(c,"enterCancelled",s),Gt(i,"delayLeave",(function(t){O=t}))}}return p}}},HM=B({tag:String,moveClass:String},kM);delete HM.mode;var GM={props:HM,beforeMount:function(){var t=this,e=this._update;this._update=function(n,o){var p=Se(t);t.__patch__(t._vnode,t.kept,!1,!0),t._vnode=t.kept,p(),e.call(t,n,o)}},render:function(t){for(var e=this.tag||this.$vnode.data.tag||"span",n=Object.create(null),o=this.prevChildren=this.children,p=this.$slots.default||[],M=this.children=[],b=DM(this),c=0;c-1?Bo[t]=e.constructor===window.HTMLUnknownElement||e.constructor===window.HTMLElement:Bo[t]=/HTMLUnknownElement/.test(e.toString())},B(no.options.directives,xM),B(no.options.components,KM),no.prototype.__patch__=K?LM:w,no.prototype.$mount=function(t,e){return function(t,e,n){var o;t.$el=e,t.$options.render||(t.$options.render=ut),Ie(t,"beforeMount"),o=function(){t._update(t._render(),n)},new vn(t,o,w,{before:function(){t._isMounted&&!t._isDestroyed&&Ie(t,"beforeUpdate")}},!0),n=!1;var p=t._preWatchers;if(p)for(var M=0;M\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,rb=/^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,zb="[a-zA-Z_][\\-\\.0-9_a-zA-Z".concat(H.source,"]*"),ab="((?:".concat(zb,"\\:)?").concat(zb,")"),ib=new RegExp("^<".concat(ab)),Ob=/^\s*(\/?)>/,sb=new RegExp("^<\\/".concat(ab,"[^>]*>")),Ab=/^]+>/i,ub=/^",""":'"',"&":"&"," ":"\n"," ":"\t","'":"'"},hb=/&(?:lt|gt|quot|amp|#39);/g,Wb=/&(?:lt|gt|quot|amp|#39|#10|#9);/g,vb=f("pre,textarea",!0),Rb=function(t,e){return t&&vb(t)&&"\n"===e[0]};function mb(t,e){var n=e?Wb:hb;return t.replace(n,(function(t){return qb[t]}))}function gb(t,e){for(var n,o,p=[],M=e.expectHTML,b=e.isUnaryTag||S,c=e.canBeLeftOpenTag||S,r=0,z=function(){if(n=t,o&&db(o)){var z=0,O=o.toLowerCase(),s=fb[O]||(fb[O]=new RegExp("([\\s\\S]*?)(]*>)","i"));v=t.replace(s,(function(t,n,o){return z=o.length,db(O)||"noscript"===O||(n=n.replace(//g,"$1").replace(//g,"$1")),Rb(O,n)&&(n=n.slice(1)),e.chars&&e.chars(n),""}));r+=t.length-v.length,t=v,i(O,r-z,r)}else{var A=t.indexOf("<");if(0===A){if(ub.test(t)){var u=t.indexOf("--\x3e");if(u>=0)return e.shouldKeepComment&&e.comment&&e.comment(t.substring(4,u),r,r+u+3),a(u+3),"continue"}if(lb.test(t)){var l=t.indexOf("]>");if(l>=0)return a(l+2),"continue"}var d=t.match(Ab);if(d)return a(d[0].length),"continue";var f=t.match(sb);if(f){var q=r;return a(f[0].length),i(f[1],q,r),"continue"}var h=function(){var e=t.match(ib);if(e){var n={tagName:e[1],attrs:[],start:r};a(e[0].length);for(var o=void 0,p=void 0;!(o=t.match(Ob))&&(p=t.match(rb)||t.match(cb));)p.start=r,a(p[0].length),p.end=r,n.attrs.push(p);if(o)return n.unarySlash=o[1],a(o[0].length),n.end=r,n}}();if(h)return function(t){var n=t.tagName,r=t.unarySlash;M&&("p"===o&&bb(n)&&i(o),c(n)&&o===n&&i(n));for(var z=b(n)||!!r,a=t.attrs.length,O=new Array(a),s=0;s=0){for(v=t.slice(A);!(sb.test(v)||ib.test(v)||ub.test(v)||lb.test(v)||(R=v.indexOf("<",1))<0);)A+=R,v=t.slice(A);W=t.substring(0,A)}A<0&&(W=t),W&&a(W.length),e.chars&&W&&e.chars(W,r-W.length,r)}if(t===n)return e.chars&&e.chars(t),"break"};t;){if("break"===z())break}function a(e){r+=e,t=t.substring(e)}function i(t,n,M){var b,c;if(null==n&&(n=r),null==M&&(M=r),t)for(c=t.toLowerCase(),b=p.length-1;b>=0&&p[b].lowerCasedTag!==c;b--);else b=0;if(b>=0){for(var z=p.length-1;z>=b;z--)e.end&&e.end(p[z].tag,n,M);p.length=b,o=b&&p[b-1].tag}else"br"===c?e.start&&e.start(t,[],!0,n,M):"p"===c&&(e.start&&e.start(t,[],!1,n,M),e.end&&e.end(t,n,M))}i()}var Lb,yb,_b,Nb,Eb,Tb,Bb,Cb,wb=/^@|^v-on:/,Sb=/^v-|^@|^:|^#/,Xb=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,xb=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,kb=/^\(|\)$/g,Ib=/^\[.*\]$/,Db=/:(.*)$/,Pb=/^:|^\.|^v-bind:/,Ub=/\.[^.\]]+(?=[^\]]*$)/g,jb=/^v-slot(:|$)|^#/,Fb=/[\r\n]/,Hb=/[ \f\t\r\n]+/g,Gb=m(ob),Yb="_empty_";function $b(t,e,n){return{type:1,tag:t,attrsList:e,attrsMap:ec(e),rawAttrsMap:{},parent:n,children:[]}}function Vb(t,e){Lb=e.warn||ip,Tb=e.isPreTag||S,Bb=e.mustUseProp||S,Cb=e.getTagNamespace||S;var n=e.isReservedTag||S;_b=Op(e.modules,"transformNode"),Nb=Op(e.modules,"preTransformNode"),Eb=Op(e.modules,"postTransformNode"),yb=e.delimiters;var o,p,M=[],b=!1!==e.preserveWhitespace,c=e.whitespace,r=!1,z=!1;function a(t){if(i(t),r||t.processed||(t=Kb(t,e)),M.length||t===o||o.if&&(t.elseif||t.else)&&Jb(o,{exp:t.elseif,block:t}),p&&!t.forbidden)if(t.elseif||t.else)b=t,c=function(t){for(var e=t.length;e--;){if(1===t[e].type)return t[e];t.pop()}}(p.children),c&&c.if&&Jb(c,{exp:b.elseif,block:b});else{if(t.slotScope){var n=t.slotTarget||'"default"';(p.scopedSlots||(p.scopedSlots={}))[n]=t}p.children.push(t),t.parent=p}var b,c;t.children=t.children.filter((function(t){return!t.slotScope})),i(t),t.pre&&(r=!1),Tb(t.tag)&&(z=!1);for(var a=0;ar&&(c.push(M=t.slice(r,p)),b.push(JSON.stringify(M)));var z=zp(o[1].trim());b.push("_s(".concat(z,")")),c.push({"@binding":z}),r=p+o[0].length}return r-1")+("true"===M?":(".concat(e,")"):":_q(".concat(e,",").concat(M,")"))),fp(t,"change","var $$a=".concat(e,",")+"$$el=$event.target,"+"$$c=$$el.checked?(".concat(M,"):(").concat(b,");")+"if(Array.isArray($$a)){"+"var $$v=".concat(o?"_n("+p+")":p,",")+"$$i=_i($$a,$$v);"+"if($$el.checked){$$i<0&&(".concat(mp(e,"$$a.concat([$$v])"),")}")+"else{$$i>-1&&(".concat(mp(e,"$$a.slice(0,$$i).concat($$a.slice($$i+1))"),")}")+"}else{".concat(mp(e,"$$c"),"}"),null,!0)}(t,o,p);else if("input"===M&&"radio"===b)!function(t,e,n){var o=n&&n.number,p=qp(t,"value")||"null";p=o?"_n(".concat(p,")"):p,sp(t,"checked","_q(".concat(e,",").concat(p,")")),fp(t,"change",mp(e,p),null,!0)}(t,o,p);else if("input"===M||"textarea"===M)!function(t,e,n){var o=t.attrsMap.type;0;var p=n||{},M=p.lazy,b=p.number,c=p.trim,r=!M&&"range"!==o,z=M?"change":"range"===o?Tp:"input",a="$event.target.value";c&&(a="$event.target.value.trim()");b&&(a="_n(".concat(a,")"));var i=mp(e,a);r&&(i="if($event.target.composing)return;".concat(i));sp(t,"value","(".concat(e,")")),fp(t,z,i,null,!0),(c||b)&&fp(t,"blur","$forceUpdate()")}(t,o,p);else{if(!F.isReservedTag(M))return Rp(t,o,p),!1}return!0},text:function(t,e){e.value&&sp(t,"textContent","_s(".concat(e.value,")"),e)},html:function(t,e){e.value&&sp(t,"innerHTML","_s(".concat(e.value,")"),e)}},ac={expectHTML:!0,modules:bc,directives:zc,isPreTag:function(t){return"pre"===t},isUnaryTag:pb,mustUseProp:so,canBeLeftOpenTag:Mb,isReservedTag:Eo,getTagNamespace:To,staticKeys:function(t){return t.reduce((function(t,e){return t.concat(e.staticKeys||[])}),[]).join(",")}(bc)},ic=m((function(t){return f("type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap"+(t?","+t:""))}));function Oc(t,e){t&&(cc=ic(e.staticKeys||""),rc=e.isReservedTag||S,sc(t),Ac(t,!1))}function sc(t){if(t.static=function(t){if(2===t.type)return!1;if(3===t.type)return!0;return!(!t.pre&&(t.hasBindings||t.if||t.for||q(t.tag)||!rc(t.tag)||function(t){for(;t.parent;){if("template"!==(t=t.parent).tag)return!1;if(t.for)return!0}return!1}(t)||!Object.keys(t).every(cc)))}(t),1===t.type){if(!rc(t.tag)&&"slot"!==t.tag&&null==t.attrsMap["inline-template"])return;for(var e=0,n=t.children.length;e|^function(?:\s+[\w$]+)?\s*\(/,lc=/\([^)]*?\);*$/,dc=/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/,fc={esc:27,tab:9,enter:13,space:32,up:38,left:37,right:39,down:40,delete:[8,46]},qc={esc:["Esc","Escape"],tab:"Tab",enter:"Enter",space:[" ","Spacebar"],up:["Up","ArrowUp"],left:["Left","ArrowLeft"],right:["Right","ArrowRight"],down:["Down","ArrowDown"],delete:["Backspace","Delete","Del"]},hc=function(t){return"if(".concat(t,")return null;")},Wc={stop:"$event.stopPropagation();",prevent:"$event.preventDefault();",self:hc("$event.target !== $event.currentTarget"),ctrl:hc("!$event.ctrlKey"),shift:hc("!$event.shiftKey"),alt:hc("!$event.altKey"),meta:hc("!$event.metaKey"),left:hc("'button' in $event && $event.button !== 0"),middle:hc("'button' in $event && $event.button !== 1"),right:hc("'button' in $event && $event.button !== 2")};function vc(t,e){var n=e?"nativeOn:":"on:",o="",p="";for(var M in t){var b=Rc(t[M]);t[M]&&t[M].dynamic?p+="".concat(M,",").concat(b,","):o+='"'.concat(M,'":').concat(b,",")}return o="{".concat(o.slice(0,-1),"}"),p?n+"_d(".concat(o,",[").concat(p.slice(0,-1),"])"):n+o}function Rc(t){if(!t)return"function(){}";if(Array.isArray(t))return"[".concat(t.map((function(t){return Rc(t)})).join(","),"]");var e=dc.test(t.value),n=uc.test(t.value),o=dc.test(t.value.replace(lc,""));if(t.modifiers){var p="",M="",b=[],c=function(e){if(Wc[e])M+=Wc[e],fc[e]&&b.push(e);else if("exact"===e){var n=t.modifiers;M+=hc(["ctrl","shift","alt","meta"].filter((function(t){return!n[t]})).map((function(t){return"$event.".concat(t,"Key")})).join("||"))}else b.push(e)};for(var r in t.modifiers)c(r);b.length&&(p+=function(t){return"if(!$event.type.indexOf('key')&&"+"".concat(t.map(mc).join("&&"),")return null;")}(b)),M&&(p+=M);var z=e?"return ".concat(t.value,".apply(null, arguments)"):n?"return (".concat(t.value,").apply(null, arguments)"):o?"return ".concat(t.value):t.value;return"function($event){".concat(p).concat(z,"}")}return e||n?t.value:"function($event){".concat(o?"return ".concat(t.value):t.value,"}")}function mc(t){var e=parseInt(t,10);if(e)return"$event.keyCode!==".concat(e);var n=fc[t],o=qc[t];return"_k($event.keyCode,"+"".concat(JSON.stringify(t),",")+"".concat(JSON.stringify(n),",")+"$event.key,"+"".concat(JSON.stringify(o))+")"}var gc={on:function(t,e){t.wrapListeners=function(t){return"_g(".concat(t,",").concat(e.value,")")}},bind:function(t,e){t.wrapData=function(n){return"_b(".concat(n,",'").concat(t.tag,"',").concat(e.value,",").concat(e.modifiers&&e.modifiers.prop?"true":"false").concat(e.modifiers&&e.modifiers.sync?",true":"",")")}},cloak:w},Lc=function(t){this.options=t,this.warn=t.warn||ip,this.transforms=Op(t.modules,"transformCode"),this.dataGenFns=Op(t.modules,"genData"),this.directives=B(B({},gc),t.directives);var e=t.isReservedTag||S;this.maybeComponent=function(t){return!!t.component||!e(t.tag)},this.onceId=0,this.staticRenderFns=[],this.pre=!1};function yc(t,e){var n=new Lc(e),o=t?"script"===t.tag?"null":_c(t,n):'_c("div")';return{render:"with(this){return ".concat(o,"}"),staticRenderFns:n.staticRenderFns}}function _c(t,e){if(t.parent&&(t.pre=t.pre||t.parent.pre),t.staticRoot&&!t.staticProcessed)return Nc(t,e);if(t.once&&!t.onceProcessed)return Ec(t,e);if(t.for&&!t.forProcessed)return Cc(t,e);if(t.if&&!t.ifProcessed)return Tc(t,e);if("template"!==t.tag||t.slotTarget||e.pre){if("slot"===t.tag)return function(t,e){var n=t.slotName||'"default"',o=xc(t,e),p="_t(".concat(n).concat(o?",function(){return ".concat(o,"}"):""),M=t.attrs||t.dynamicAttrs?Dc((t.attrs||[]).concat(t.dynamicAttrs||[]).map((function(t){return{name:L(t.name),value:t.value,dynamic:t.dynamic}}))):null,b=t.attrsMap["v-bind"];!M&&!b||o||(p+=",null");M&&(p+=",".concat(M));b&&(p+="".concat(M?"":",null",",").concat(b));return p+")"}(t,e);var n=void 0;if(t.component)n=function(t,e,n){var o=e.inlineTemplate?null:xc(e,n,!0);return"_c(".concat(t,",").concat(wc(e,n)).concat(o?",".concat(o):"",")")}(t.component,t,e);else{var o=void 0,p=e.maybeComponent(t);(!t.plain||t.pre&&p)&&(o=wc(t,e));var M=void 0,b=e.options.bindings;p&&b&&!1!==b.__isScriptSetup&&(M=function(t,e){var n=L(e),o=y(n),p=function(p){return t[e]===p?e:t[n]===p?n:t[o]===p?o:void 0},M=p("setup-const")||p("setup-reactive-const");if(M)return M;var b=p("setup-let")||p("setup-ref")||p("setup-maybe-ref");if(b)return b}(b,t.tag)),M||(M="'".concat(t.tag,"'"));var c=t.inlineTemplate?null:xc(t,e,!0);n="_c(".concat(M).concat(o?",".concat(o):"").concat(c?",".concat(c):"",")")}for(var r=0;r>>0}(b)):"",")")}(t,t.scopedSlots,e),",")),t.model&&(n+="model:{value:".concat(t.model.value,",callback:").concat(t.model.callback,",expression:").concat(t.model.expression,"},")),t.inlineTemplate){var M=function(t,e){var n=t.children[0];0;if(n&&1===n.type){var o=yc(n,e.options);return"inlineTemplate:{render:function(){".concat(o.render,"},staticRenderFns:[").concat(o.staticRenderFns.map((function(t){return"function(){".concat(t,"}")})).join(","),"]}")}}(t,e);M&&(n+="".concat(M,","))}return n=n.replace(/,$/,"")+"}",t.dynamicAttrs&&(n="_b(".concat(n,',"').concat(t.tag,'",').concat(Dc(t.dynamicAttrs),")")),t.wrapData&&(n=t.wrapData(n)),t.wrapListeners&&(n=t.wrapListeners(n)),n}function Sc(t){return 1===t.type&&("slot"===t.tag||t.children.some(Sc))}function Xc(t,e){var n=t.attrsMap["slot-scope"];if(t.if&&!t.ifProcessed&&!n)return Tc(t,e,Xc,"null");if(t.for&&!t.forProcessed)return Cc(t,e,Xc);var o=t.slotScope===Yb?"":String(t.slotScope),p="function(".concat(o,"){")+"return ".concat("template"===t.tag?t.if&&n?"(".concat(t.if,")?").concat(xc(t,e)||"undefined",":undefined"):xc(t,e)||"undefined":_c(t,e),"}"),M=o?"":",proxy:true";return"{key:".concat(t.slotTarget||'"default"',",fn:").concat(p).concat(M,"}")}function xc(t,e,n,o,p){var M=t.children;if(M.length){var b=M[0];if(1===M.length&&b.for&&"template"!==b.tag&&"slot"!==b.tag){var c=n?e.maybeComponent(b)?",1":",0":"";return"".concat((o||_c)(b,e)).concat(c)}var r=n?function(t,e){for(var n=0,o=0;o':'
',Hc.innerHTML.indexOf(" ")>0}var Vc=!!K&&$c(!1),Kc=!!K&&$c(!0),Qc=m((function(t){var e=wo(t);return e&&e.innerHTML})),Jc=no.prototype.$mount;no.prototype.$mount=function(t,e){if((t=t&&wo(t))===document.body||t===document.documentElement)return this;var n=this.$options;if(!n.render){var o=n.template;if(o)if("string"==typeof o)"#"===o.charAt(0)&&(o=Qc(o));else{if(!o.nodeType)return this;o=o.innerHTML}else t&&(o=function(t){if(t.outerHTML)return t.outerHTML;var e=document.createElement("div");return e.appendChild(t.cloneNode(!0)),e.innerHTML}(t));if(o){0;var p=Yc(o,{outputSourceRange:!1,shouldDecodeNewlines:Vc,shouldDecodeNewlinesForHref:Kc,delimiters:n.delimiters,comments:n.comments},this),M=p.render,b=p.staticRenderFns;n.render=M,n.staticRenderFns=b}}return Jc.call(this,t,e)},no.compile=Yc;var Zc=n(2543),tr=n.n(Zc),er=n(4743),nr=n.n(er);const or={computed:{Telescope:function(t){function e(){return t.apply(this,arguments)}return e.toString=function(){return t.toString()},e}((function(){return Telescope}))},methods:{timeAgo:function(t){nr().updateLocale("en",{relativeTime:{future:"in %s",past:"%s ago",s:function(t){return t+"s ago"},ss:"%ds ago",m:"1m ago",mm:"%dm ago",h:"1h ago",hh:"%dh ago",d:"1d ago",dd:"%dd ago",M:"a month ago",MM:"%d months ago",y:"a year ago",yy:"%d years ago"}});var e=nr()().diff(t,"seconds"),n=nr()("2018-01-01").startOf("day").seconds(e);return e>300?nr()(t).fromNow(!0):e<60?n.format("s")+"s ago":n.format("m:ss")+"m ago"},localTime:function(t){return nr()(t).local().format("MMMM Do YYYY, h:mm:ss A")},truncate:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:70;return tr().truncate(t,{length:e,separator:/,? +/})},debouncer:tr().debounce((function(t){return t()}),500),alertError:function(t){this.$root.alert.type="error",this.$root.alert.autoClose=!1,this.$root.alert.message=t},alertSuccess:function(t,e){this.$root.alert.type="success",this.$root.alert.autoClose=e,this.$root.alert.message=t},alertConfirm:function(t,e,n){this.$root.alert.type="confirmation",this.$root.alert.autoClose=!1,this.$root.alert.message=t,this.$root.alert.confirmationProceed=e,this.$root.alert.confirmationCancel=n}}};var pr=n(4335);const Mr=[{path:"/",redirect:"/requests"},{path:"/mail/:id",name:"mail-preview",component:n(583).A},{path:"/mail",name:"mail",component:n(1574).A},{path:"/exceptions/:id",name:"exception-preview",component:n(3781).A},{path:"/exceptions",name:"exceptions",component:n(2977).A},{path:"/dumps",name:"dumps",component:n(93).A},{path:"/logs/:id",name:"log-preview",component:n(5356).A},{path:"/logs",name:"logs",component:n(8170).A},{path:"/notifications/:id",name:"notification-preview",component:n(5841).A},{path:"/notifications",name:"notifications",component:n(4969).A},{path:"/jobs/:id",name:"job-preview",component:n(8813).A},{path:"/jobs",name:"jobs",component:n(1202).A},{path:"/batches/:id",name:"batch-preview",component:n(9622).A},{path:"/batches",name:"batches",component:n(8888).A},{path:"/events/:id",name:"event-preview",component:n(1119).A},{path:"/events",name:"events",component:n(7380).A},{path:"/cache/:id",name:"cache-preview",component:n(7362).A},{path:"/cache",name:"cache",component:n(8613).A},{path:"/queries/:id",name:"query-preview",component:n(1891).A},{path:"/queries",name:"queries",component:n(5873).A},{path:"/models/:id",name:"model-preview",component:n(5333).A},{path:"/models",name:"models",component:n(9440).A},{path:"/requests/:id",name:"request-preview",component:n(6025).A},{path:"/requests",name:"requests",component:n(2806).A},{path:"/commands/:id",name:"command-preview",component:n(4145).A},{path:"/commands",name:"commands",component:n(346).A},{path:"/schedule/:id",name:"schedule-preview",component:n(9655).A},{path:"/schedule",name:"schedule",component:n(3524).A},{path:"/redis/:id",name:"redis-preview",component:n(6393).A},{path:"/redis",name:"redis",component:n(8872).A},{path:"/monitored-tags",name:"monitored-tags",component:n(5441).A},{path:"/gates/:id",name:"gate-preview",component:n(1905).A},{path:"/gates",name:"gates",component:n(2707).A},{path:"/views/:id",name:"view-preview",component:n(6703).A},{path:"/views",name:"views",component:n(3308).A},{path:"/client-requests/:id",name:"client-request-preview",component:n(6204).A},{path:"/client-requests",name:"client-requests",component:n(5899).A}];function br(t,e){for(var n in e)t[n]=e[n];return t}var cr=/[!'()*]/g,rr=function(t){return"%"+t.charCodeAt(0).toString(16)},zr=/%2C/g,ar=function(t){return encodeURIComponent(t).replace(cr,rr).replace(zr,",")};function ir(t){try{return decodeURIComponent(t)}catch(t){0}return t}var Or=function(t){return null==t||"object"==typeof t?t:String(t)};function sr(t){var e={};return(t=t.trim().replace(/^(\?|#|&)/,""))?(t.split("&").forEach((function(t){var n=t.replace(/\+/g," ").split("="),o=ir(n.shift()),p=n.length>0?ir(n.join("=")):null;void 0===e[o]?e[o]=p:Array.isArray(e[o])?e[o].push(p):e[o]=[e[o],p]})),e):e}function Ar(t){var e=t?Object.keys(t).map((function(e){var n=t[e];if(void 0===n)return"";if(null===n)return ar(e);if(Array.isArray(n)){var o=[];return n.forEach((function(t){void 0!==t&&(null===t?o.push(ar(e)):o.push(ar(e)+"="+ar(t)))})),o.join("&")}return ar(e)+"="+ar(n)})).filter((function(t){return t.length>0})).join("&"):null;return e?"?"+e:""}var ur=/\/?$/;function lr(t,e,n,o){var p=o&&o.options.stringifyQuery,M=e.query||{};try{M=dr(M)}catch(t){}var b={name:e.name||t&&t.name,meta:t&&t.meta||{},path:e.path||"/",hash:e.hash||"",query:M,params:e.params||{},fullPath:hr(e,p),matched:t?qr(t):[]};return n&&(b.redirectedFrom=hr(n,p)),Object.freeze(b)}function dr(t){if(Array.isArray(t))return t.map(dr);if(t&&"object"==typeof t){var e={};for(var n in t)e[n]=dr(t[n]);return e}return t}var fr=lr(null,{path:"/"});function qr(t){for(var e=[];t;)e.unshift(t),t=t.parent;return e}function hr(t,e){var n=t.path,o=t.query;void 0===o&&(o={});var p=t.hash;return void 0===p&&(p=""),(n||"/")+(e||Ar)(o)+p}function Wr(t,e,n){return e===fr?t===e:!!e&&(t.path&&e.path?t.path.replace(ur,"")===e.path.replace(ur,"")&&(n||t.hash===e.hash&&vr(t.query,e.query)):!(!t.name||!e.name)&&(t.name===e.name&&(n||t.hash===e.hash&&vr(t.query,e.query)&&vr(t.params,e.params))))}function vr(t,e){if(void 0===t&&(t={}),void 0===e&&(e={}),!t||!e)return t===e;var n=Object.keys(t).sort(),o=Object.keys(e).sort();return n.length===o.length&&n.every((function(n,p){var M=t[n];if(o[p]!==n)return!1;var b=e[n];return null==M||null==b?M===b:"object"==typeof M&&"object"==typeof b?vr(M,b):String(M)===String(b)}))}function Rr(t){for(var e=0;e=0&&(e=t.slice(o),t=t.slice(0,o));var p=t.indexOf("?");return p>=0&&(n=t.slice(p+1),t=t.slice(0,p)),{path:t,query:n,hash:e}}(p.path||""),z=e&&e.path||"/",a=r.path?Lr(r.path,z,n||p.append):z,i=function(t,e,n){void 0===e&&(e={});var o,p=n||sr;try{o=p(t||"")}catch(t){o={}}for(var M in e){var b=e[M];o[M]=Array.isArray(b)?b.map(Or):Or(b)}return o}(r.query,p.query,o&&o.options.parseQuery),O=p.hash||r.hash;return O&&"#"!==O.charAt(0)&&(O="#"+O),{_normalized:!0,path:a,query:i,hash:O}}var $r,Vr=function(){},Kr={name:"RouterLink",props:{to:{type:[String,Object],required:!0},tag:{type:String,default:"a"},custom:Boolean,exact:Boolean,exactPath:Boolean,append:Boolean,replace:Boolean,activeClass:String,exactActiveClass:String,ariaCurrentValue:{type:String,default:"page"},event:{type:[String,Array],default:"click"}},render:function(t){var e=this,n=this.$router,o=this.$route,p=n.resolve(this.to,o,this.append),M=p.location,b=p.route,c=p.href,r={},z=n.options.linkActiveClass,a=n.options.linkExactActiveClass,i=null==z?"router-link-active":z,O=null==a?"router-link-exact-active":a,s=null==this.activeClass?i:this.activeClass,A=null==this.exactActiveClass?O:this.exactActiveClass,u=b.redirectedFrom?lr(null,Yr(b.redirectedFrom),null,n):b;r[A]=Wr(o,u,this.exactPath),r[s]=this.exact||this.exactPath?r[A]:function(t,e){return 0===t.path.replace(ur,"/").indexOf(e.path.replace(ur,"/"))&&(!e.hash||t.hash===e.hash)&&function(t,e){for(var n in e)if(!(n in t))return!1;return!0}(t.query,e.query)}(o,u);var l=r[A]?this.ariaCurrentValue:null,d=function(t){Qr(t)&&(e.replace?n.replace(M,Vr):n.push(M,Vr))},f={click:Qr};Array.isArray(this.event)?this.event.forEach((function(t){f[t]=d})):f[this.event]=d;var q={class:r},h=!this.$scopedSlots.$hasNormal&&this.$scopedSlots.default&&this.$scopedSlots.default({href:c,route:b,navigate:d,isActive:r[s],isExactActive:r[A]});if(h){if(1===h.length)return h[0];if(h.length>1||!h.length)return 0===h.length?t():t("span",{},h)}if("a"===this.tag)q.on=f,q.attrs={href:c,"aria-current":l};else{var W=Jr(this.$slots.default);if(W){W.isStatic=!1;var v=W.data=br({},W.data);for(var R in v.on=v.on||{},v.on){var m=v.on[R];R in f&&(v.on[R]=Array.isArray(m)?m:[m])}for(var g in f)g in v.on?v.on[g].push(f[g]):v.on[g]=d;var L=W.data.attrs=br({},W.data.attrs);L.href=c,L["aria-current"]=l}else q.on=f}return t(this.tag,q,this.$slots.default)}};function Qr(t){if(!(t.metaKey||t.altKey||t.ctrlKey||t.shiftKey||t.defaultPrevented||void 0!==t.button&&0!==t.button)){if(t.currentTarget&&t.currentTarget.getAttribute){var e=t.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(e))return}return t.preventDefault&&t.preventDefault(),!0}}function Jr(t){if(t)for(var e,n=0;n-1&&(c.params[O]=n.params[O]);return c.path=Gr(a.path,c.params),r(a,c,b)}if(c.path){c.params={};for(var s=0;s-1}function Ez(t,e){return Nz(t)&&t._isRouter&&(null==e||t.type===e)}function Tz(t,e,n){var o=function(p){p>=t.length?n():t[p]?e(t[p],(function(){o(p+1)})):o(p+1)};o(0)}function Bz(t){return function(e,n,o){var p=!1,M=0,b=null;Cz(t,(function(t,e,n,c){if("function"==typeof t&&void 0===t.cid){p=!0,M++;var r,z=Xz((function(e){var p;((p=e).__esModule||Sz&&"Module"===p[Symbol.toStringTag])&&(e=e.default),t.resolved="function"==typeof e?e:$r.extend(e),n.components[c]=e,--M<=0&&o()})),a=Xz((function(t){var e="Failed to resolve async component "+c+": "+t;b||(b=Nz(t)?t:new Error(e),o(b))}));try{r=t(z,a)}catch(t){a(t)}if(r)if("function"==typeof r.then)r.then(z,a);else{var i=r.component;i&&"function"==typeof i.then&&i.then(z,a)}}})),p||o()}}function Cz(t,e){return wz(t.map((function(t){return Object.keys(t.components).map((function(n){return e(t.components[n],t.instances[n],t,n)}))})))}function wz(t){return Array.prototype.concat.apply([],t)}var Sz="function"==typeof Symbol&&"symbol"==typeof Symbol.toStringTag;function Xz(t){var e=!1;return function(){for(var n=[],o=arguments.length;o--;)n[o]=arguments[o];if(!e)return e=!0,t.apply(this,n)}}var xz=function(t,e){this.router=t,this.base=function(t){if(!t)if(Zr){var e=document.querySelector("base");t=(t=e&&e.getAttribute("href")||"/").replace(/^https?:\/\/[^\/]+/,"")}else t="/";"/"!==t.charAt(0)&&(t="/"+t);return t.replace(/\/$/,"")}(e),this.current=fr,this.pending=null,this.ready=!1,this.readyCbs=[],this.readyErrorCbs=[],this.errorCbs=[],this.listeners=[]};function kz(t,e,n,o){var p=Cz(t,(function(t,o,p,M){var b=function(t,e){"function"!=typeof t&&(t=$r.extend(t));return t.options[e]}(t,e);if(b)return Array.isArray(b)?b.map((function(t){return n(t,o,p,M)})):n(b,o,p,M)}));return wz(o?p.reverse():p)}function Iz(t,e){if(e)return function(){return t.apply(e,arguments)}}xz.prototype.listen=function(t){this.cb=t},xz.prototype.onReady=function(t,e){this.ready?t():(this.readyCbs.push(t),e&&this.readyErrorCbs.push(e))},xz.prototype.onError=function(t){this.errorCbs.push(t)},xz.prototype.transitionTo=function(t,e,n){var o,p=this;try{o=this.router.match(t,this.current)}catch(t){throw this.errorCbs.forEach((function(e){e(t)})),t}var M=this.current;this.confirmTransition(o,(function(){p.updateRoute(o),e&&e(o),p.ensureURL(),p.router.afterHooks.forEach((function(t){t&&t(o,M)})),p.ready||(p.ready=!0,p.readyCbs.forEach((function(t){t(o)})))}),(function(t){n&&n(t),t&&!p.ready&&(Ez(t,mz.redirected)&&M===fr||(p.ready=!0,p.readyErrorCbs.forEach((function(e){e(t)}))))}))},xz.prototype.confirmTransition=function(t,e,n){var o=this,p=this.current;this.pending=t;var M,b,c=function(t){!Ez(t)&&Nz(t)&&o.errorCbs.length&&o.errorCbs.forEach((function(e){e(t)})),n&&n(t)},r=t.matched.length-1,z=p.matched.length-1;if(Wr(t,p)&&r===z&&t.matched[r]===p.matched[z])return this.ensureURL(),t.hash&&Oz(this.router,p,t,!1),c(((b=yz(M=p,t,mz.duplicated,'Avoided redundant navigation to current location: "'+M.fullPath+'".')).name="NavigationDuplicated",b));var a=function(t,e){var n,o=Math.max(t.length,e.length);for(n=0;n0)){var e=this.router,n=e.options.scrollBehavior,o=Wz&&n;o&&this.listeners.push(iz());var p=function(){var n=t.current,p=Pz(t.base);t.current===fr&&p===t._startLocation||t.transitionTo(p,(function(t){o&&Oz(e,t,n,!0)}))};window.addEventListener("popstate",p),this.listeners.push((function(){window.removeEventListener("popstate",p)}))}},e.prototype.go=function(t){window.history.go(t)},e.prototype.push=function(t,e,n){var o=this,p=this.current;this.transitionTo(t,(function(t){vz(yr(o.base+t.fullPath)),Oz(o.router,t,p,!1),e&&e(t)}),n)},e.prototype.replace=function(t,e,n){var o=this,p=this.current;this.transitionTo(t,(function(t){Rz(yr(o.base+t.fullPath)),Oz(o.router,t,p,!1),e&&e(t)}),n)},e.prototype.ensureURL=function(t){if(Pz(this.base)!==this.current.fullPath){var e=yr(this.base+this.current.fullPath);t?vz(e):Rz(e)}},e.prototype.getCurrentLocation=function(){return Pz(this.base)},e}(xz);function Pz(t){var e=window.location.pathname,n=e.toLowerCase(),o=t.toLowerCase();return!t||n!==o&&0!==n.indexOf(yr(o+"/"))||(e=e.slice(t.length)),(e||"/")+window.location.search+window.location.hash}var Uz=function(t){function e(e,n,o){t.call(this,e,n),o&&function(t){var e=Pz(t);if(!/^\/#/.test(e))return window.location.replace(yr(t+"/#"+e)),!0}(this.base)||jz()}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.setupListeners=function(){var t=this;if(!(this.listeners.length>0)){var e=this.router.options.scrollBehavior,n=Wz&&e;n&&this.listeners.push(iz());var o=function(){var e=t.current;jz()&&t.transitionTo(Fz(),(function(o){n&&Oz(t.router,o,e,!0),Wz||Yz(o.fullPath)}))},p=Wz?"popstate":"hashchange";window.addEventListener(p,o),this.listeners.push((function(){window.removeEventListener(p,o)}))}},e.prototype.push=function(t,e,n){var o=this,p=this.current;this.transitionTo(t,(function(t){Gz(t.fullPath),Oz(o.router,t,p,!1),e&&e(t)}),n)},e.prototype.replace=function(t,e,n){var o=this,p=this.current;this.transitionTo(t,(function(t){Yz(t.fullPath),Oz(o.router,t,p,!1),e&&e(t)}),n)},e.prototype.go=function(t){window.history.go(t)},e.prototype.ensureURL=function(t){var e=this.current.fullPath;Fz()!==e&&(t?Gz(e):Yz(e))},e.prototype.getCurrentLocation=function(){return Fz()},e}(xz);function jz(){var t=Fz();return"/"===t.charAt(0)||(Yz("/"+t),!1)}function Fz(){var t=window.location.href,e=t.indexOf("#");return e<0?"":t=t.slice(e+1)}function Hz(t){var e=window.location.href,n=e.indexOf("#");return(n>=0?e.slice(0,n):e)+"#"+t}function Gz(t){Wz?vz(Hz(t)):window.location.hash=t}function Yz(t){Wz?Rz(Hz(t)):window.location.replace(Hz(t))}var $z=function(t){function e(e,n){t.call(this,e,n),this.stack=[],this.index=-1}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.push=function(t,e,n){var o=this;this.transitionTo(t,(function(t){o.stack=o.stack.slice(0,o.index+1).concat(t),o.index++,e&&e(t)}),n)},e.prototype.replace=function(t,e,n){var o=this;this.transitionTo(t,(function(t){o.stack=o.stack.slice(0,o.index).concat(t),e&&e(t)}),n)},e.prototype.go=function(t){var e=this,n=this.index+t;if(!(n<0||n>=this.stack.length)){var o=this.stack[n];this.confirmTransition(o,(function(){var t=e.current;e.index=n,e.updateRoute(o),e.router.afterHooks.forEach((function(e){e&&e(o,t)}))}),(function(t){Ez(t,mz.duplicated)&&(e.index=n)}))}},e.prototype.getCurrentLocation=function(){var t=this.stack[this.stack.length-1];return t?t.fullPath:"/"},e.prototype.ensureURL=function(){},e}(xz),Vz=function(t){void 0===t&&(t={}),this.app=null,this.apps=[],this.options=t,this.beforeHooks=[],this.resolveHooks=[],this.afterHooks=[],this.matcher=oz(t.routes||[],this);var e=t.mode||"hash";switch(this.fallback="history"===e&&!Wz&&!1!==t.fallback,this.fallback&&(e="hash"),Zr||(e="abstract"),this.mode=e,e){case"history":this.history=new Dz(this,t.base);break;case"hash":this.history=new Uz(this,t.base,this.fallback);break;case"abstract":this.history=new $z(this,t.base)}},Kz={currentRoute:{configurable:!0}};Vz.prototype.match=function(t,e,n){return this.matcher.match(t,e,n)},Kz.currentRoute.get=function(){return this.history&&this.history.current},Vz.prototype.init=function(t){var e=this;if(this.apps.push(t),t.$once("hook:destroyed",(function(){var n=e.apps.indexOf(t);n>-1&&e.apps.splice(n,1),e.app===t&&(e.app=e.apps[0]||null),e.app||e.history.teardown()})),!this.app){this.app=t;var n=this.history;if(n instanceof Dz||n instanceof Uz){var o=function(t){n.setupListeners(),function(t){var o=n.current,p=e.options.scrollBehavior;Wz&&p&&"fullPath"in t&&Oz(e,t,o,!1)}(t)};n.transitionTo(n.getCurrentLocation(),o,o)}n.listen((function(t){e.apps.forEach((function(e){e._route=t}))}))}},Vz.prototype.beforeEach=function(t){return Jz(this.beforeHooks,t)},Vz.prototype.beforeResolve=function(t){return Jz(this.resolveHooks,t)},Vz.prototype.afterEach=function(t){return Jz(this.afterHooks,t)},Vz.prototype.onReady=function(t,e){this.history.onReady(t,e)},Vz.prototype.onError=function(t){this.history.onError(t)},Vz.prototype.push=function(t,e,n){var o=this;if(!e&&!n&&"undefined"!=typeof Promise)return new Promise((function(e,n){o.history.push(t,e,n)}));this.history.push(t,e,n)},Vz.prototype.replace=function(t,e,n){var o=this;if(!e&&!n&&"undefined"!=typeof Promise)return new Promise((function(e,n){o.history.replace(t,e,n)}));this.history.replace(t,e,n)},Vz.prototype.go=function(t){this.history.go(t)},Vz.prototype.back=function(){this.go(-1)},Vz.prototype.forward=function(){this.go(1)},Vz.prototype.getMatchedComponents=function(t){var e=t?t.matched?t:this.resolve(t).route:this.currentRoute;return e?[].concat.apply([],e.matched.map((function(t){return Object.keys(t.components).map((function(e){return t.components[e]}))}))):[]},Vz.prototype.resolve=function(t,e,n){var o=Yr(t,e=e||this.history.current,n,this),p=this.match(o,e),M=p.redirectedFrom||p.fullPath,b=function(t,e,n){var o="hash"===n?"#"+e:e;return t?yr(t+"/"+o):o}(this.history.base,M,this.mode);return{location:o,route:p,href:b,normalizedTo:o,resolved:p}},Vz.prototype.getRoutes=function(){return this.matcher.getRoutes()},Vz.prototype.addRoute=function(t,e){this.matcher.addRoute(t,e),this.history.current!==fr&&this.history.transitionTo(this.history.getCurrentLocation())},Vz.prototype.addRoutes=function(t){this.matcher.addRoutes(t),this.history.current!==fr&&this.history.transitionTo(this.history.getCurrentLocation())},Object.defineProperties(Vz.prototype,Kz);var Qz=Vz;function Jz(t,e){return t.push(e),function(){var n=t.indexOf(e);n>-1&&t.splice(n,1)}}Vz.install=function t(e){if(!t.installed||$r!==e){t.installed=!0,$r=e;var n=function(t){return void 0!==t},o=function(t,e){var o=t.$options._parentVnode;n(o)&&n(o=o.data)&&n(o=o.registerRouteInstance)&&o(t,e)};e.mixin({beforeCreate:function(){n(this.$options.router)?(this._routerRoot=this,this._router=this.$options.router,this._router.init(this),e.util.defineReactive(this,"_route",this._router.history.current)):this._routerRoot=this.$parent&&this.$parent._routerRoot||this,o(this,this)},destroyed:function(){o(this)}}),Object.defineProperty(e.prototype,"$router",{get:function(){return this._routerRoot._router}}),Object.defineProperty(e.prototype,"$route",{get:function(){return this._routerRoot._route}}),e.component("RouterView",mr),e.component("RouterLink",Kr);var p=e.config.optionMergeStrategies;p.beforeRouteEnter=p.beforeRouteLeave=p.beforeRouteUpdate=p.created}},Vz.version="3.6.5",Vz.isNavigationFailure=Ez,Vz.NavigationFailureType=mz,Vz.START_LOCATION=fr,Zr&&window.Vue&&window.Vue.use(Vz);var Zz=n(7551),ta=n.n(Zz),ea=n(5072),na=n.n(ea),oa=n(2930),pa={insert:"head",singleton:!1};na()(oa.A,pa);oa.A.locals;n(2754);var Ma=document.head.querySelector('meta[name="csrf-token"]');Ma&&(pr.A.defaults.headers.common["X-CSRF-TOKEN"]=Ma.content),no.use(Qz),window.Popper=n(8851).default,nr().tz.setDefault(Telescope.timezone),window.Telescope.basePath="/"+window.Telescope.path;var ba=window.Telescope.basePath+"/";""!==window.Telescope.path&&"/"!==window.Telescope.path||(ba="/",window.Telescope.basePath="");var ca=new Qz({routes:Mr,mode:"history",base:ba});no.component("vue-json-pretty",ta()),no.component("related-entries",n(4401).A),no.component("index-screen",n(4980).A),no.component("preview-screen",n(9416).A),no.component("alert",n(4445).A),no.component("copy-clipboard",n(1858).A),no.mixin(or),new no({el:"#telescope",router:ca,data:function(){return{alert:{type:null,autoClose:0,message:"",confirmationProceed:null,confirmationCancel:null},autoLoadsNewEntries:"1"===localStorage.autoLoadsNewEntries,recording:Telescope.recording}},created:function(){window.addEventListener("keydown",this.keydownListener)},destroyed:function(){window.removeEventListener("keydown",this.keydownListener)},methods:{autoLoadNewEntries:function(){this.autoLoadsNewEntries?(this.autoLoadsNewEntries=!1,localStorage.autoLoadsNewEntries=0):(this.autoLoadsNewEntries=!0,localStorage.autoLoadsNewEntries=1)},toggleRecording:function(){pr.A.post(Telescope.basePath+"/telescope-api/toggle-recording"),window.Telescope.recording=!Telescope.recording,this.recording=!this.recording},clearEntries:function(){(!(arguments.length>0&&void 0!==arguments[0])||arguments[0])&&!confirm("Are you sure you want to delete all Telescope data?")||pr.A.delete(Telescope.basePath+"/telescope-api/entries").then((function(t){return location.reload()}))},keydownListener:function(t){t.metaKey&&"k"===t.key&&this.clearEntries(!1)}}})},8217:(t,e,n)=>{"use strict";n.d(e,{A:()=>o});const o={methods:{cacheActionTypeClass:function(t){return"hit"===t?"success":"set"===t?"info":"forget"===t?"warning":"missed"===t?"danger":void 0},composerTypeClass:function(t){return"composer"===t?"info":"creator"===t?"success":void 0},gateResultClass:function(t){return"allowed"===t?"success":"denied"===t?"danger":void 0},jobStatusClass:function(t){return"pending"===t?"secondary":"processed"===t?"success":"failed"===t?"danger":void 0},logLevelClass:function(t){return"debug"===t?"success":"info"===t?"info":"notice"===t?"secondary":"warning"===t?"warning":"error"===t||"critical"===t||"alert"===t||"emergency"===t?"danger":void 0},modelActionClass:function(t){return"created"==t?"success":"updated"==t?"info":"retrieved"==t?"secondary":"deleted"==t||"forceDeleted"==t?"danger":void 0},requestStatusClass:function(t){return t?t<300?"success":t<400?"info":t<500?"warning":t>=500?"danger":void 0:"danger"},requestMethodClass:function(t){return"GET"==t||"OPTIONS"==t?"secondary":"POST"==t||"PATCH"==t||"PUT"==t?"info":"DELETE"==t?"danger":void 0}}}},7526:(t,e)=>{"use strict";e.byteLength=function(t){var e=c(t),n=e[0],o=e[1];return 3*(n+o)/4-o},e.toByteArray=function(t){var e,n,M=c(t),b=M[0],r=M[1],z=new p(function(t,e,n){return 3*(e+n)/4-n}(0,b,r)),a=0,i=r>0?b-4:b;for(n=0;n>16&255,z[a++]=e>>8&255,z[a++]=255&e;2===r&&(e=o[t.charCodeAt(n)]<<2|o[t.charCodeAt(n+1)]>>4,z[a++]=255&e);1===r&&(e=o[t.charCodeAt(n)]<<10|o[t.charCodeAt(n+1)]<<4|o[t.charCodeAt(n+2)]>>2,z[a++]=e>>8&255,z[a++]=255&e);return z},e.fromByteArray=function(t){for(var e,o=t.length,p=o%3,M=[],b=16383,c=0,z=o-p;cz?z:c+b));1===p?(e=t[o-1],M.push(n[e>>2]+n[e<<4&63]+"==")):2===p&&(e=(t[o-2]<<8)+t[o-1],M.push(n[e>>10]+n[e>>4&63]+n[e<<2&63]+"="));return M.join("")};for(var n=[],o=[],p="undefined"!=typeof Uint8Array?Uint8Array:Array,M="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",b=0;b<64;++b)n[b]=M[b],o[M.charCodeAt(b)]=b;function c(t){var e=t.length;if(e%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var n=t.indexOf("=");return-1===n&&(n=e),[n,n===e?0:4-n%4]}function r(t,e,o){for(var p,M,b=[],c=e;c>18&63]+n[M>>12&63]+n[M>>6&63]+n[63&M]);return b.join("")}o["-".charCodeAt(0)]=62,o["_".charCodeAt(0)]=63},2754:function(t,e,n){!function(t,e,n){"use strict";function o(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var p=o(e),M=o(n);function b(t,e){for(var n=0;n=b)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}};f.jQueryDetection(),d();var q="alert",h="4.6.2",W="bs.alert",v="."+W,R=".data-api",m=p.default.fn[q],g="alert",L="fade",y="show",_="close"+v,N="closed"+v,E="click"+v+R,T='[data-dismiss="alert"]',B=function(){function t(t){this._element=t}var e=t.prototype;return e.close=function(t){var e=this._element;t&&(e=this._getRootElement(t)),this._triggerCloseEvent(e).isDefaultPrevented()||this._removeElement(e)},e.dispose=function(){p.default.removeData(this._element,W),this._element=null},e._getRootElement=function(t){var e=f.getSelectorFromElement(t),n=!1;return e&&(n=document.querySelector(e)),n||(n=p.default(t).closest("."+g)[0]),n},e._triggerCloseEvent=function(t){var e=p.default.Event(_);return p.default(t).trigger(e),e},e._removeElement=function(t){var e=this;if(p.default(t).removeClass(y),p.default(t).hasClass(L)){var n=f.getTransitionDurationFromElement(t);p.default(t).one(f.TRANSITION_END,(function(n){return e._destroyElement(t,n)})).emulateTransitionEnd(n)}else this._destroyElement(t)},e._destroyElement=function(t){p.default(t).detach().trigger(N).remove()},t._jQueryInterface=function(e){return this.each((function(){var n=p.default(this),o=n.data(W);o||(o=new t(this),n.data(W,o)),"close"===e&&o[e](this)}))},t._handleDismiss=function(t){return function(e){e&&e.preventDefault(),t.close(this)}},c(t,null,[{key:"VERSION",get:function(){return h}}]),t}();p.default(document).on(E,T,B._handleDismiss(new B)),p.default.fn[q]=B._jQueryInterface,p.default.fn[q].Constructor=B,p.default.fn[q].noConflict=function(){return p.default.fn[q]=m,B._jQueryInterface};var C="button",w="4.6.2",S="bs.button",X="."+S,x=".data-api",k=p.default.fn[C],I="active",D="btn",P="focus",U="click"+X+x,j="focus"+X+x+" blur"+X+x,F="load"+X+x,H='[data-toggle^="button"]',G='[data-toggle="buttons"]',Y='[data-toggle="button"]',$='[data-toggle="buttons"] .btn',V='input:not([type="hidden"])',K=".active",Q=".btn",J=function(){function t(t){this._element=t,this.shouldAvoidTriggerChange=!1}var e=t.prototype;return e.toggle=function(){var t=!0,e=!0,n=p.default(this._element).closest(G)[0];if(n){var o=this._element.querySelector(V);if(o){if("radio"===o.type)if(o.checked&&this._element.classList.contains(I))t=!1;else{var M=n.querySelector(K);M&&p.default(M).removeClass(I)}t&&("checkbox"!==o.type&&"radio"!==o.type||(o.checked=!this._element.classList.contains(I)),this.shouldAvoidTriggerChange||p.default(o).trigger("change")),o.focus(),e=!1}}this._element.hasAttribute("disabled")||this._element.classList.contains("disabled")||(e&&this._element.setAttribute("aria-pressed",!this._element.classList.contains(I)),t&&p.default(this._element).toggleClass(I))},e.dispose=function(){p.default.removeData(this._element,S),this._element=null},t._jQueryInterface=function(e,n){return this.each((function(){var o=p.default(this),M=o.data(S);M||(M=new t(this),o.data(S,M)),M.shouldAvoidTriggerChange=n,"toggle"===e&&M[e]()}))},c(t,null,[{key:"VERSION",get:function(){return w}}]),t}();p.default(document).on(U,H,(function(t){var e=t.target,n=e;if(p.default(e).hasClass(D)||(e=p.default(e).closest(Q)[0]),!e||e.hasAttribute("disabled")||e.classList.contains("disabled"))t.preventDefault();else{var o=e.querySelector(V);if(o&&(o.hasAttribute("disabled")||o.classList.contains("disabled")))return void t.preventDefault();"INPUT"!==n.tagName&&"LABEL"===e.tagName||J._jQueryInterface.call(p.default(e),"toggle","INPUT"===n.tagName)}})).on(j,H,(function(t){var e=p.default(t.target).closest(Q)[0];p.default(e).toggleClass(P,/^focus(in)?$/.test(t.type))})),p.default(window).on(F,(function(){for(var t=[].slice.call(document.querySelectorAll($)),e=0,n=t.length;e0,this._pointerEvent=Boolean(window.PointerEvent||window.MSPointerEvent),this._addEventListeners()}var e=t.prototype;return e.next=function(){this._isSliding||this._slide(dt)},e.nextWhenVisible=function(){var t=p.default(this._element);!document.hidden&&t.is(":visible")&&"hidden"!==t.css("visibility")&&this.next()},e.prev=function(){this._isSliding||this._slide(ft)},e.pause=function(t){t||(this._isPaused=!0),this._element.querySelector(kt)&&(f.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},e.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},e.to=function(t){var e=this;this._activeElement=this._element.querySelector(St);var n=this._getItemIndex(this._activeElement);if(!(t>this._items.length-1||t<0))if(this._isSliding)p.default(this._element).one(vt,(function(){return e.to(t)}));else{if(n===t)return this.pause(),void this.cycle();var o=t>n?dt:ft;this._slide(o,this._items[t])}},e.dispose=function(){p.default(this._element).off(nt),p.default.removeData(this._element,et),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},e._getConfig=function(t){return t=r({},Ut,t),f.typeCheckConfig(Z,t,jt),t},e._handleSwipe=function(){var t=Math.abs(this.touchDeltaX);if(!(t<=rt)){var e=t/this.touchDeltaX;this.touchDeltaX=0,e>0&&this.prev(),e<0&&this.next()}},e._addEventListeners=function(){var t=this;this._config.keyboard&&p.default(this._element).on(Rt,(function(e){return t._keydown(e)})),"hover"===this._config.pause&&p.default(this._element).on(mt,(function(e){return t.pause(e)})).on(gt,(function(e){return t.cycle(e)})),this._config.touch&&this._addTouchEventListeners()},e._addTouchEventListeners=function(){var t=this;if(this._touchSupported){var e=function(e){t._pointerEvent&&Ft[e.originalEvent.pointerType.toUpperCase()]?t.touchStartX=e.originalEvent.clientX:t._pointerEvent||(t.touchStartX=e.originalEvent.touches[0].clientX)},n=function(e){t.touchDeltaX=e.originalEvent.touches&&e.originalEvent.touches.length>1?0:e.originalEvent.touches[0].clientX-t.touchStartX},o=function(e){t._pointerEvent&&Ft[e.originalEvent.pointerType.toUpperCase()]&&(t.touchDeltaX=e.originalEvent.clientX-t.touchStartX),t._handleSwipe(),"hover"===t._config.pause&&(t.pause(),t.touchTimeout&&clearTimeout(t.touchTimeout),t.touchTimeout=setTimeout((function(e){return t.cycle(e)}),ct+t._config.interval))};p.default(this._element.querySelectorAll(xt)).on(Tt,(function(t){return t.preventDefault()})),this._pointerEvent?(p.default(this._element).on(Nt,(function(t){return e(t)})),p.default(this._element).on(Et,(function(t){return o(t)})),this._element.classList.add(lt)):(p.default(this._element).on(Lt,(function(t){return e(t)})),p.default(this._element).on(yt,(function(t){return n(t)})),p.default(this._element).on(_t,(function(t){return o(t)})))}},e._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.which){case Mt:t.preventDefault(),this.prev();break;case bt:t.preventDefault(),this.next()}},e._getItemIndex=function(t){return this._items=t&&t.parentNode?[].slice.call(t.parentNode.querySelectorAll(Xt)):[],this._items.indexOf(t)},e._getItemByDirection=function(t,e){var n=t===dt,o=t===ft,p=this._getItemIndex(e),M=this._items.length-1;if((o&&0===p||n&&p===M)&&!this._config.wrap)return e;var b=(p+(t===ft?-1:1))%this._items.length;return-1===b?this._items[this._items.length-1]:this._items[b]},e._triggerSlideEvent=function(t,e){var n=this._getItemIndex(t),o=this._getItemIndex(this._element.querySelector(St)),M=p.default.Event(Wt,{relatedTarget:t,direction:e,from:o,to:n});return p.default(this._element).trigger(M),M},e._setActiveIndicatorElement=function(t){if(this._indicatorsElement){var e=[].slice.call(this._indicatorsElement.querySelectorAll(wt));p.default(e).removeClass(at);var n=this._indicatorsElement.children[this._getItemIndex(t)];n&&p.default(n).addClass(at)}},e._updateInterval=function(){var t=this._activeElement||this._element.querySelector(St);if(t){var e=parseInt(t.getAttribute("data-interval"),10);e?(this._config.defaultInterval=this._config.defaultInterval||this._config.interval,this._config.interval=e):this._config.interval=this._config.defaultInterval||this._config.interval}},e._slide=function(t,e){var n,o,M,b=this,c=this._element.querySelector(St),r=this._getItemIndex(c),z=e||c&&this._getItemByDirection(t,c),a=this._getItemIndex(z),i=Boolean(this._interval);if(t===dt?(n=st,o=At,M=qt):(n=Ot,o=ut,M=ht),z&&p.default(z).hasClass(at))this._isSliding=!1;else if(!this._triggerSlideEvent(z,M).isDefaultPrevented()&&c&&z){this._isSliding=!0,i&&this.pause(),this._setActiveIndicatorElement(z),this._activeElement=z;var O=p.default.Event(vt,{relatedTarget:z,direction:M,from:r,to:a});if(p.default(this._element).hasClass(it)){p.default(z).addClass(o),f.reflow(z),p.default(c).addClass(n),p.default(z).addClass(n);var s=f.getTransitionDurationFromElement(c);p.default(c).one(f.TRANSITION_END,(function(){p.default(z).removeClass(n+" "+o).addClass(at),p.default(c).removeClass(at+" "+o+" "+n),b._isSliding=!1,setTimeout((function(){return p.default(b._element).trigger(O)}),0)})).emulateTransitionEnd(s)}else p.default(c).removeClass(at),p.default(z).addClass(at),this._isSliding=!1,p.default(this._element).trigger(O);i&&this.cycle()}},t._jQueryInterface=function(e){return this.each((function(){var n=p.default(this).data(et),o=r({},Ut,p.default(this).data());"object"==typeof e&&(o=r({},o,e));var M="string"==typeof e?e:o.slide;if(n||(n=new t(this,o),p.default(this).data(et,n)),"number"==typeof e)n.to(e);else if("string"==typeof M){if(void 0===n[M])throw new TypeError('No method named "'+M+'"');n[M]()}else o.interval&&o.ride&&(n.pause(),n.cycle())}))},t._dataApiClickHandler=function(e){var n=f.getSelectorFromElement(this);if(n){var o=p.default(n)[0];if(o&&p.default(o).hasClass(zt)){var M=r({},p.default(o).data(),p.default(this).data()),b=this.getAttribute("data-slide-to");b&&(M.interval=!1),t._jQueryInterface.call(p.default(o),M),b&&p.default(o).data(et).to(b),e.preventDefault()}}},c(t,null,[{key:"VERSION",get:function(){return tt}},{key:"Default",get:function(){return Ut}}]),t}();p.default(document).on(Ct,Dt,Ht._dataApiClickHandler),p.default(window).on(Bt,(function(){for(var t=[].slice.call(document.querySelectorAll(Pt)),e=0,n=t.length;e0&&(this._selector=b,this._triggerArray.push(M))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}var e=t.prototype;return e.toggle=function(){p.default(this._element).hasClass(Jt)?this.hide():this.show()},e.show=function(){var e,n,o=this;if(!(this._isTransitioning||p.default(this._element).hasClass(Jt)||(this._parent&&0===(e=[].slice.call(this._parent.querySelectorAll(ze)).filter((function(t){return"string"==typeof o._config.parent?t.getAttribute("data-parent")===o._config.parent:t.classList.contains(Zt)}))).length&&(e=null),e&&(n=p.default(e).not(this._selector).data($t))&&n._isTransitioning))){var M=p.default.Event(pe);if(p.default(this._element).trigger(M),!M.isDefaultPrevented()){e&&(t._jQueryInterface.call(p.default(e).not(this._selector),"hide"),n||p.default(e).data($t,null));var b=this._getDimension();p.default(this._element).removeClass(Zt).addClass(te),this._element.style[b]=0,this._triggerArray.length&&p.default(this._triggerArray).removeClass(ee).attr("aria-expanded",!0),this.setTransitioning(!0);var c=function(){p.default(o._element).removeClass(te).addClass(Zt+" "+Jt),o._element.style[b]="",o.setTransitioning(!1),p.default(o._element).trigger(Me)},r="scroll"+(b[0].toUpperCase()+b.slice(1)),z=f.getTransitionDurationFromElement(this._element);p.default(this._element).one(f.TRANSITION_END,c).emulateTransitionEnd(z),this._element.style[b]=this._element[r]+"px"}}},e.hide=function(){var t=this;if(!this._isTransitioning&&p.default(this._element).hasClass(Jt)){var e=p.default.Event(be);if(p.default(this._element).trigger(e),!e.isDefaultPrevented()){var n=this._getDimension();this._element.style[n]=this._element.getBoundingClientRect()[n]+"px",f.reflow(this._element),p.default(this._element).addClass(te).removeClass(Zt+" "+Jt);var o=this._triggerArray.length;if(o>0)for(var M=0;M0},e._getOffset=function(){var t=this,e={};return"function"==typeof this._config.offset?e.fn=function(e){return e.offsets=r({},e.offsets,t._config.offset(e.offsets,t._element)),e}:e.offset=this._config.offset,e},e._getPopperConfig=function(){var t={placement:this._getPlacement(),modifiers:{offset:this._getOffset(),flip:{enabled:this._config.flip},preventOverflow:{boundariesElement:this._config.boundary}}};return"static"===this._config.display&&(t.modifiers.applyStyle={enabled:!1}),r({},t,this._config.popperConfig)},t._jQueryInterface=function(e){return this.each((function(){var n=p.default(this).data(le);if(n||(n=new t(this,"object"==typeof e?e:null),p.default(this).data(le,n)),"string"==typeof e){if(void 0===n[e])throw new TypeError('No method named "'+e+'"');n[e]()}}))},t._clearMenus=function(e){if(!e||e.which!==ge&&("keyup"!==e.type||e.which===ve))for(var n=[].slice.call(document.querySelectorAll(Ue)),o=0,M=n.length;o0&&b--,e.which===me&&bdocument.documentElement.clientHeight;n||(this._element.style.overflowY="hidden"),this._element.classList.add(ln);var o=f.getTransitionDurationFromElement(this._dialog);p.default(this._element).off(f.TRANSITION_END),p.default(this._element).one(f.TRANSITION_END,(function(){t._element.classList.remove(ln),n||p.default(t._element).one(f.TRANSITION_END,(function(){t._element.style.overflowY=""})).emulateTransitionEnd(t._element,o)})).emulateTransitionEnd(o),this._element.focus()}},e._showElement=function(t){var e=this,n=p.default(this._element).hasClass(An),o=this._dialog?this._dialog.querySelector(En):null;this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.appendChild(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),p.default(this._dialog).hasClass(zn)&&o?o.scrollTop=0:this._element.scrollTop=0,n&&f.reflow(this._element),p.default(this._element).addClass(un),this._config.focus&&this._enforceFocus();var M=p.default.Event(Wn,{relatedTarget:t}),b=function(){e._config.focus&&e._element.focus(),e._isTransitioning=!1,p.default(e._element).trigger(M)};if(n){var c=f.getTransitionDurationFromElement(this._dialog);p.default(this._dialog).one(f.TRANSITION_END,b).emulateTransitionEnd(c)}else b()},e._enforceFocus=function(){var t=this;p.default(document).off(vn).on(vn,(function(e){document!==e.target&&t._element!==e.target&&0===p.default(t._element).has(e.target).length&&t._element.focus()}))},e._setEscapeEvent=function(){var t=this;this._isShown?p.default(this._element).on(gn,(function(e){t._config.keyboard&&e.which===rn?(e.preventDefault(),t.hide()):t._config.keyboard||e.which!==rn||t._triggerBackdropTransition()})):this._isShown||p.default(this._element).off(gn)},e._setResizeEvent=function(){var t=this;this._isShown?p.default(window).on(Rn,(function(e){return t.handleUpdate(e)})):p.default(window).off(Rn)},e._hideModal=function(){var t=this;this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._showBackdrop((function(){p.default(document.body).removeClass(sn),t._resetAdjustments(),t._resetScrollbar(),p.default(t._element).trigger(qn)}))},e._removeBackdrop=function(){this._backdrop&&(p.default(this._backdrop).remove(),this._backdrop=null)},e._showBackdrop=function(t){var e=this,n=p.default(this._element).hasClass(An)?An:"";if(this._isShown&&this._config.backdrop){if(this._backdrop=document.createElement("div"),this._backdrop.className=On,n&&this._backdrop.classList.add(n),p.default(this._backdrop).appendTo(document.body),p.default(this._element).on(mn,(function(t){e._ignoreBackdropClick?e._ignoreBackdropClick=!1:t.target===t.currentTarget&&("static"===e._config.backdrop?e._triggerBackdropTransition():e.hide())})),n&&f.reflow(this._backdrop),p.default(this._backdrop).addClass(un),!t)return;if(!n)return void t();var o=f.getTransitionDurationFromElement(this._backdrop);p.default(this._backdrop).one(f.TRANSITION_END,t).emulateTransitionEnd(o)}else if(!this._isShown&&this._backdrop){p.default(this._backdrop).removeClass(un);var M=function(){e._removeBackdrop(),t&&t()};if(p.default(this._element).hasClass(An)){var b=f.getTransitionDurationFromElement(this._backdrop);p.default(this._backdrop).one(f.TRANSITION_END,M).emulateTransitionEnd(b)}else M()}else t&&t()},e._adjustDialog=function(){var t=this._element.scrollHeight>document.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},e._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},e._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=Math.round(t.left+t.right)
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",customClass:"",sanitize:!0,sanitizeFn:null,whiteList:In,popperConfig:null},ao={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(number|string|function)",container:"(string|element|boolean)",fallbackPlacement:"(string|array)",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",whiteList:"object",popperConfig:"(null|object)"},io={HIDE:"hide"+Yn,HIDDEN:"hidden"+Yn,SHOW:"show"+Yn,SHOWN:"shown"+Yn,INSERTED:"inserted"+Yn,CLICK:"click"+Yn,FOCUSIN:"focusin"+Yn,FOCUSOUT:"focusout"+Yn,MOUSEENTER:"mouseenter"+Yn,MOUSELEAVE:"mouseleave"+Yn},Oo=function(){function t(t,e){if(void 0===M.default)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var e=t.prototype;return e.enable=function(){this._isEnabled=!0},e.disable=function(){this._isEnabled=!1},e.toggleEnabled=function(){this._isEnabled=!this._isEnabled},e.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=p.default(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),p.default(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(p.default(this.getTipElement()).hasClass(Zn))return void this._leave(null,this);this._enter(null,this)}},e.dispose=function(){clearTimeout(this._timeout),p.default.removeData(this.element,this.constructor.DATA_KEY),p.default(this.element).off(this.constructor.EVENT_KEY),p.default(this.element).closest(".modal").off("hide.bs.modal",this._hideModalHandler),this.tip&&p.default(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},e.show=function(){var t=this;if("none"===p.default(this.element).css("display"))throw new Error("Please use show on visible elements");var e=p.default.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){p.default(this.element).trigger(e);var n=f.findShadowRoot(this.element),o=p.default.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(e.isDefaultPrevented()||!o)return;var b=this.getTipElement(),c=f.getUID(this.constructor.NAME);b.setAttribute("id",c),this.element.setAttribute("aria-describedby",c),this.setContent(),this.config.animation&&p.default(b).addClass(Jn);var r="function"==typeof this.config.placement?this.config.placement.call(this,b,this.element):this.config.placement,z=this._getAttachment(r);this.addAttachmentClass(z);var a=this._getContainer();p.default(b).data(this.constructor.DATA_KEY,this),p.default.contains(this.element.ownerDocument.documentElement,this.tip)||p.default(b).appendTo(a),p.default(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new M.default(this.element,b,this._getPopperConfig(z)),p.default(b).addClass(Zn),p.default(b).addClass(this.config.customClass),"ontouchstart"in document.documentElement&&p.default(document.body).children().on("mouseover",null,p.default.noop);var i=function(){t.config.animation&&t._fixTransition();var e=t._hoverState;t._hoverState=null,p.default(t.element).trigger(t.constructor.Event.SHOWN),e===eo&&t._leave(null,t)};if(p.default(this.tip).hasClass(Jn)){var O=f.getTransitionDurationFromElement(this.tip);p.default(this.tip).one(f.TRANSITION_END,i).emulateTransitionEnd(O)}else i()}},e.hide=function(t){var e=this,n=this.getTipElement(),o=p.default.Event(this.constructor.Event.HIDE),M=function(){e._hoverState!==to&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),p.default(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(p.default(this.element).trigger(o),!o.isDefaultPrevented()){if(p.default(n).removeClass(Zn),"ontouchstart"in document.documentElement&&p.default(document.body).children().off("mouseover",null,p.default.noop),this._activeTrigger[bo]=!1,this._activeTrigger[Mo]=!1,this._activeTrigger[po]=!1,p.default(this.tip).hasClass(Jn)){var b=f.getTransitionDurationFromElement(n);p.default(n).one(f.TRANSITION_END,M).emulateTransitionEnd(b)}else M();this._hoverState=""}},e.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},e.isWithContent=function(){return Boolean(this.getTitle())},e.addAttachmentClass=function(t){p.default(this.getTipElement()).addClass(Vn+"-"+t)},e.getTipElement=function(){return this.tip=this.tip||p.default(this.config.template)[0],this.tip},e.setContent=function(){var t=this.getTipElement();this.setElementContent(p.default(t.querySelectorAll(no)),this.getTitle()),p.default(t).removeClass(Jn+" "+Zn)},e.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=jn(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?p.default(e).parent().is(t)||t.empty().append(e):t.text(p.default(e).text())},e.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},e._getPopperConfig=function(t){var e=this;return r({},{placement:t,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:oo},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}},this.config.popperConfig)},e._getOffset=function(){var t=this,e={};return"function"==typeof this.config.offset?e.fn=function(e){return e.offsets=r({},e.offsets,t.config.offset(e.offsets,t.element)),e}:e.offset=this.config.offset,e},e._getContainer=function(){return!1===this.config.container?document.body:f.isElement(this.config.container)?p.default(this.config.container):p.default(document).find(this.config.container)},e._getAttachment=function(t){return ro[t.toUpperCase()]},e._setListeners=function(){var t=this;this.config.trigger.split(" ").forEach((function(e){if("click"===e)p.default(t.element).on(t.constructor.Event.CLICK,t.config.selector,(function(e){return t.toggle(e)}));else if(e!==co){var n=e===po?t.constructor.Event.MOUSEENTER:t.constructor.Event.FOCUSIN,o=e===po?t.constructor.Event.MOUSELEAVE:t.constructor.Event.FOCUSOUT;p.default(t.element).on(n,t.config.selector,(function(e){return t._enter(e)})).on(o,t.config.selector,(function(e){return t._leave(e)}))}})),this._hideModalHandler=function(){t.element&&t.hide()},p.default(this.element).closest(".modal").on("hide.bs.modal",this._hideModalHandler),this.config.selector?this.config=r({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},e._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},e._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||p.default(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),p.default(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Mo:po]=!0),p.default(e.getTipElement()).hasClass(Zn)||e._hoverState===to?e._hoverState=to:(clearTimeout(e._timeout),e._hoverState=to,e.config.delay&&e.config.delay.show?e._timeout=setTimeout((function(){e._hoverState===to&&e.show()}),e.config.delay.show):e.show())},e._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||p.default(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),p.default(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Mo:po]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=eo,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout((function(){e._hoverState===eo&&e.hide()}),e.config.delay.hide):e.hide())},e._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},e._getConfig=function(t){var e=p.default(this.element).data();return Object.keys(e).forEach((function(t){-1!==Qn.indexOf(t)&&delete e[t]})),"number"==typeof(t=r({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),f.typeCheckConfig(Fn,t,this.constructor.DefaultType),t.sanitize&&(t.template=jn(t.template,t.whiteList,t.sanitizeFn)),t},e._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},e._cleanTipClass=function(){var t=p.default(this.getTipElement()),e=t.attr("class").match(Kn);null!==e&&e.length&&t.removeClass(e.join(""))},e._handlePopperPlacementChange=function(t){this.tip=t.instance.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},e._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(p.default(t).removeClass(Jn),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},t._jQueryInterface=function(e){return this.each((function(){var n=p.default(this),o=n.data(Gn),M="object"==typeof e&&e;if((o||!/dispose|hide/.test(e))&&(o||(o=new t(this,M),n.data(Gn,o)),"string"==typeof e)){if(void 0===o[e])throw new TypeError('No method named "'+e+'"');o[e]()}}))},c(t,null,[{key:"VERSION",get:function(){return Hn}},{key:"Default",get:function(){return zo}},{key:"NAME",get:function(){return Fn}},{key:"DATA_KEY",get:function(){return Gn}},{key:"Event",get:function(){return io}},{key:"EVENT_KEY",get:function(){return Yn}},{key:"DefaultType",get:function(){return ao}}]),t}();p.default.fn[Fn]=Oo._jQueryInterface,p.default.fn[Fn].Constructor=Oo,p.default.fn[Fn].noConflict=function(){return p.default.fn[Fn]=$n,Oo._jQueryInterface};var so="popover",Ao="4.6.2",uo="bs.popover",lo="."+uo,fo=p.default.fn[so],qo="bs-popover",ho=new RegExp("(^|\\s)"+qo+"\\S+","g"),Wo="fade",vo="show",Ro=".popover-header",mo=".popover-body",go=r({},Oo.Default,{placement:"right",trigger:"click",content:"",template:''}),Lo=r({},Oo.DefaultType,{content:"(string|element|function)"}),yo={HIDE:"hide"+lo,HIDDEN:"hidden"+lo,SHOW:"show"+lo,SHOWN:"shown"+lo,INSERTED:"inserted"+lo,CLICK:"click"+lo,FOCUSIN:"focusin"+lo,FOCUSOUT:"focusout"+lo,MOUSEENTER:"mouseenter"+lo,MOUSELEAVE:"mouseleave"+lo},_o=function(t){function e(){return t.apply(this,arguments)||this}z(e,t);var n=e.prototype;return n.isWithContent=function(){return this.getTitle()||this._getContent()},n.addAttachmentClass=function(t){p.default(this.getTipElement()).addClass(qo+"-"+t)},n.getTipElement=function(){return this.tip=this.tip||p.default(this.config.template)[0],this.tip},n.setContent=function(){var t=p.default(this.getTipElement());this.setElementContent(t.find(Ro),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(mo),e),t.removeClass(Wo+" "+vo)},n._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},n._cleanTipClass=function(){var t=p.default(this.getTipElement()),e=t.attr("class").match(ho);null!==e&&e.length>0&&t.removeClass(e.join(""))},e._jQueryInterface=function(t){return this.each((function(){var n=p.default(this).data(uo),o="object"==typeof t?t:null;if((n||!/dispose|hide/.test(t))&&(n||(n=new e(this,o),p.default(this).data(uo,n)),"string"==typeof t)){if(void 0===n[t])throw new TypeError('No method named "'+t+'"');n[t]()}}))},c(e,null,[{key:"VERSION",get:function(){return Ao}},{key:"Default",get:function(){return go}},{key:"NAME",get:function(){return so}},{key:"DATA_KEY",get:function(){return uo}},{key:"Event",get:function(){return yo}},{key:"EVENT_KEY",get:function(){return lo}},{key:"DefaultType",get:function(){return Lo}}]),e}(Oo);p.default.fn[so]=_o._jQueryInterface,p.default.fn[so].Constructor=_o,p.default.fn[so].noConflict=function(){return p.default.fn[so]=fo,_o._jQueryInterface};var No="scrollspy",Eo="4.6.2",To="bs.scrollspy",Bo="."+To,Co=".data-api",wo=p.default.fn[No],So="dropdown-item",Xo="active",xo="activate"+Bo,ko="scroll"+Bo,Io="load"+Bo+Co,Do="offset",Po="position",Uo='[data-spy="scroll"]',jo=".nav, .list-group",Fo=".nav-link",Ho=".nav-item",Go=".list-group-item",Yo=".dropdown",$o=".dropdown-item",Vo=".dropdown-toggle",Ko={offset:10,method:"auto",target:""},Qo={offset:"number",method:"string",target:"(string|element)"},Jo=function(){function t(t,e){var n=this;this._element=t,this._scrollElement="BODY"===t.tagName?window:t,this._config=this._getConfig(e),this._selector=this._config.target+" "+Fo+","+this._config.target+" "+Go+","+this._config.target+" "+$o,this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,p.default(this._scrollElement).on(ko,(function(t){return n._process(t)})),this.refresh(),this._process()}var e=t.prototype;return e.refresh=function(){var t=this,e=this._scrollElement===this._scrollElement.window?Do:Po,n="auto"===this._config.method?e:this._config.method,o=n===Po?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),[].slice.call(document.querySelectorAll(this._selector)).map((function(t){var e,M=f.getSelectorFromElement(t);if(M&&(e=document.querySelector(M)),e){var b=e.getBoundingClientRect();if(b.width||b.height)return[p.default(e)[n]().top+o,M]}return null})).filter(Boolean).sort((function(t,e){return t[0]-e[0]})).forEach((function(e){t._offsets.push(e[0]),t._targets.push(e[1])}))},e.dispose=function(){p.default.removeData(this._element,To),p.default(this._scrollElement).off(Bo),this._element=null,this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},e._getConfig=function(t){if("string"!=typeof(t=r({},Ko,"object"==typeof t&&t?t:{})).target&&f.isElement(t.target)){var e=p.default(t.target).attr("id");e||(e=f.getUID(No),p.default(t.target).attr("id",e)),t.target="#"+e}return f.typeCheckConfig(No,t,Qo),t},e._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},e._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},e._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},e._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=n){var o=this._targets[this._targets.length-1];this._activeTarget!==o&&this._activate(o)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var p=this._offsets.length;p--;)this._activeTarget!==this._targets[p]&&t>=this._offsets[p]&&(void 0===this._offsets[p+1]||t{"use strict";var o=n(7526),p=n(251),M=n(4634);function b(){return r.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function c(t,e){if(b()=b())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+b().toString(16)+" bytes");return 0|t}function A(t,e){if(r.isBuffer(t))return t.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(t)||t instanceof ArrayBuffer))return t.byteLength;"string"!=typeof t&&(t=""+t);var n=t.length;if(0===n)return 0;for(var o=!1;;)switch(e){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":case void 0:return P(t).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return U(t).length;default:if(o)return P(t).length;e=(""+e).toLowerCase(),o=!0}}function u(t,e,n){var o=!1;if((void 0===e||e<0)&&(e=0),e>this.length)return"";if((void 0===n||n>this.length)&&(n=this.length),n<=0)return"";if((n>>>=0)<=(e>>>=0))return"";for(t||(t="utf8");;)switch(t){case"hex":return E(this,e,n);case"utf8":case"utf-8":return L(this,e,n);case"ascii":return _(this,e,n);case"latin1":case"binary":return N(this,e,n);case"base64":return g(this,e,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return T(this,e,n);default:if(o)throw new TypeError("Unknown encoding: "+t);t=(t+"").toLowerCase(),o=!0}}function l(t,e,n){var o=t[e];t[e]=t[n],t[n]=o}function d(t,e,n,o,p){if(0===t.length)return-1;if("string"==typeof n?(o=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),n=+n,isNaN(n)&&(n=p?0:t.length-1),n<0&&(n=t.length+n),n>=t.length){if(p)return-1;n=t.length-1}else if(n<0){if(!p)return-1;n=0}if("string"==typeof e&&(e=r.from(e,o)),r.isBuffer(e))return 0===e.length?-1:f(t,e,n,o,p);if("number"==typeof e)return e&=255,r.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?p?Uint8Array.prototype.indexOf.call(t,e,n):Uint8Array.prototype.lastIndexOf.call(t,e,n):f(t,[e],n,o,p);throw new TypeError("val must be string, number or Buffer")}function f(t,e,n,o,p){var M,b=1,c=t.length,r=e.length;if(void 0!==o&&("ucs2"===(o=String(o).toLowerCase())||"ucs-2"===o||"utf16le"===o||"utf-16le"===o)){if(t.length<2||e.length<2)return-1;b=2,c/=2,r/=2,n/=2}function z(t,e){return 1===b?t[e]:t.readUInt16BE(e*b)}if(p){var a=-1;for(M=n;Mc&&(n=c-r),M=n;M>=0;M--){for(var i=!0,O=0;Op&&(o=p):o=p;var M=e.length;if(M%2!=0)throw new TypeError("Invalid hex string");o>M/2&&(o=M/2);for(var b=0;b>8,p=n%256,M.push(p),M.push(o);return M}(e,t.length-n),t,n,o)}function g(t,e,n){return 0===e&&n===t.length?o.fromByteArray(t):o.fromByteArray(t.slice(e,n))}function L(t,e,n){n=Math.min(t.length,n);for(var o=[],p=e;p239?4:z>223?3:z>191?2:1;if(p+i<=n)switch(i){case 1:z<128&&(a=z);break;case 2:128==(192&(M=t[p+1]))&&(r=(31&z)<<6|63&M)>127&&(a=r);break;case 3:M=t[p+1],b=t[p+2],128==(192&M)&&128==(192&b)&&(r=(15&z)<<12|(63&M)<<6|63&b)>2047&&(r<55296||r>57343)&&(a=r);break;case 4:M=t[p+1],b=t[p+2],c=t[p+3],128==(192&M)&&128==(192&b)&&128==(192&c)&&(r=(15&z)<<18|(63&M)<<12|(63&b)<<6|63&c)>65535&&r<1114112&&(a=r)}null===a?(a=65533,i=1):a>65535&&(a-=65536,o.push(a>>>10&1023|55296),a=56320|1023&a),o.push(a),p+=i}return function(t){var e=t.length;if(e<=y)return String.fromCharCode.apply(String,t);var n="",o=0;for(;o0&&(t=this.toString("hex",0,n).match(/.{2}/g).join(" "),this.length>n&&(t+=" ... ")),""},r.prototype.compare=function(t,e,n,o,p){if(!r.isBuffer(t))throw new TypeError("Argument must be a Buffer");if(void 0===e&&(e=0),void 0===n&&(n=t?t.length:0),void 0===o&&(o=0),void 0===p&&(p=this.length),e<0||n>t.length||o<0||p>this.length)throw new RangeError("out of range index");if(o>=p&&e>=n)return 0;if(o>=p)return-1;if(e>=n)return 1;if(this===t)return 0;for(var M=(p>>>=0)-(o>>>=0),b=(n>>>=0)-(e>>>=0),c=Math.min(M,b),z=this.slice(o,p),a=t.slice(e,n),i=0;ip)&&(n=p),t.length>0&&(n<0||e<0)||e>this.length)throw new RangeError("Attempt to write outside buffer bounds");o||(o="utf8");for(var M=!1;;)switch(o){case"hex":return q(this,t,e,n);case"utf8":case"utf-8":return h(this,t,e,n);case"ascii":return W(this,t,e,n);case"latin1":case"binary":return v(this,t,e,n);case"base64":return R(this,t,e,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return m(this,t,e,n);default:if(M)throw new TypeError("Unknown encoding: "+o);o=(""+o).toLowerCase(),M=!0}},r.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var y=4096;function _(t,e,n){var o="";n=Math.min(t.length,n);for(var p=e;po)&&(n=o);for(var p="",M=e;Mn)throw new RangeError("Trying to access beyond buffer length")}function C(t,e,n,o,p,M){if(!r.isBuffer(t))throw new TypeError('"buffer" argument must be a Buffer instance');if(e>p||et.length)throw new RangeError("Index out of range")}function w(t,e,n,o){e<0&&(e=65535+e+1);for(var p=0,M=Math.min(t.length-n,2);p>>8*(o?p:1-p)}function S(t,e,n,o){e<0&&(e=4294967295+e+1);for(var p=0,M=Math.min(t.length-n,4);p>>8*(o?p:3-p)&255}function X(t,e,n,o,p,M){if(n+o>t.length)throw new RangeError("Index out of range");if(n<0)throw new RangeError("Index out of range")}function x(t,e,n,o,M){return M||X(t,0,n,4),p.write(t,e,n,o,23,4),n+4}function k(t,e,n,o,M){return M||X(t,0,n,8),p.write(t,e,n,o,52,8),n+8}r.prototype.slice=function(t,e){var n,o=this.length;if((t=~~t)<0?(t+=o)<0&&(t=0):t>o&&(t=o),(e=void 0===e?o:~~e)<0?(e+=o)<0&&(e=0):e>o&&(e=o),e0&&(p*=256);)o+=this[t+--e]*p;return o},r.prototype.readUInt8=function(t,e){return e||B(t,1,this.length),this[t]},r.prototype.readUInt16LE=function(t,e){return e||B(t,2,this.length),this[t]|this[t+1]<<8},r.prototype.readUInt16BE=function(t,e){return e||B(t,2,this.length),this[t]<<8|this[t+1]},r.prototype.readUInt32LE=function(t,e){return e||B(t,4,this.length),(this[t]|this[t+1]<<8|this[t+2]<<16)+16777216*this[t+3]},r.prototype.readUInt32BE=function(t,e){return e||B(t,4,this.length),16777216*this[t]+(this[t+1]<<16|this[t+2]<<8|this[t+3])},r.prototype.readIntLE=function(t,e,n){t|=0,e|=0,n||B(t,e,this.length);for(var o=this[t],p=1,M=0;++M=(p*=128)&&(o-=Math.pow(2,8*e)),o},r.prototype.readIntBE=function(t,e,n){t|=0,e|=0,n||B(t,e,this.length);for(var o=e,p=1,M=this[t+--o];o>0&&(p*=256);)M+=this[t+--o]*p;return M>=(p*=128)&&(M-=Math.pow(2,8*e)),M},r.prototype.readInt8=function(t,e){return e||B(t,1,this.length),128&this[t]?-1*(255-this[t]+1):this[t]},r.prototype.readInt16LE=function(t,e){e||B(t,2,this.length);var n=this[t]|this[t+1]<<8;return 32768&n?4294901760|n:n},r.prototype.readInt16BE=function(t,e){e||B(t,2,this.length);var n=this[t+1]|this[t]<<8;return 32768&n?4294901760|n:n},r.prototype.readInt32LE=function(t,e){return e||B(t,4,this.length),this[t]|this[t+1]<<8|this[t+2]<<16|this[t+3]<<24},r.prototype.readInt32BE=function(t,e){return e||B(t,4,this.length),this[t]<<24|this[t+1]<<16|this[t+2]<<8|this[t+3]},r.prototype.readFloatLE=function(t,e){return e||B(t,4,this.length),p.read(this,t,!0,23,4)},r.prototype.readFloatBE=function(t,e){return e||B(t,4,this.length),p.read(this,t,!1,23,4)},r.prototype.readDoubleLE=function(t,e){return e||B(t,8,this.length),p.read(this,t,!0,52,8)},r.prototype.readDoubleBE=function(t,e){return e||B(t,8,this.length),p.read(this,t,!1,52,8)},r.prototype.writeUIntLE=function(t,e,n,o){(t=+t,e|=0,n|=0,o)||C(this,t,e,n,Math.pow(2,8*n)-1,0);var p=1,M=0;for(this[e]=255&t;++M=0&&(M*=256);)this[e+p]=t/M&255;return e+n},r.prototype.writeUInt8=function(t,e,n){return t=+t,e|=0,n||C(this,t,e,1,255,0),r.TYPED_ARRAY_SUPPORT||(t=Math.floor(t)),this[e]=255&t,e+1},r.prototype.writeUInt16LE=function(t,e,n){return t=+t,e|=0,n||C(this,t,e,2,65535,0),r.TYPED_ARRAY_SUPPORT?(this[e]=255&t,this[e+1]=t>>>8):w(this,t,e,!0),e+2},r.prototype.writeUInt16BE=function(t,e,n){return t=+t,e|=0,n||C(this,t,e,2,65535,0),r.TYPED_ARRAY_SUPPORT?(this[e]=t>>>8,this[e+1]=255&t):w(this,t,e,!1),e+2},r.prototype.writeUInt32LE=function(t,e,n){return t=+t,e|=0,n||C(this,t,e,4,4294967295,0),r.TYPED_ARRAY_SUPPORT?(this[e+3]=t>>>24,this[e+2]=t>>>16,this[e+1]=t>>>8,this[e]=255&t):S(this,t,e,!0),e+4},r.prototype.writeUInt32BE=function(t,e,n){return t=+t,e|=0,n||C(this,t,e,4,4294967295,0),r.TYPED_ARRAY_SUPPORT?(this[e]=t>>>24,this[e+1]=t>>>16,this[e+2]=t>>>8,this[e+3]=255&t):S(this,t,e,!1),e+4},r.prototype.writeIntLE=function(t,e,n,o){if(t=+t,e|=0,!o){var p=Math.pow(2,8*n-1);C(this,t,e,n,p-1,-p)}var M=0,b=1,c=0;for(this[e]=255&t;++M=0&&(b*=256);)t<0&&0===c&&0!==this[e+M+1]&&(c=1),this[e+M]=(t/b|0)-c&255;return e+n},r.prototype.writeInt8=function(t,e,n){return t=+t,e|=0,n||C(this,t,e,1,127,-128),r.TYPED_ARRAY_SUPPORT||(t=Math.floor(t)),t<0&&(t=255+t+1),this[e]=255&t,e+1},r.prototype.writeInt16LE=function(t,e,n){return t=+t,e|=0,n||C(this,t,e,2,32767,-32768),r.TYPED_ARRAY_SUPPORT?(this[e]=255&t,this[e+1]=t>>>8):w(this,t,e,!0),e+2},r.prototype.writeInt16BE=function(t,e,n){return t=+t,e|=0,n||C(this,t,e,2,32767,-32768),r.TYPED_ARRAY_SUPPORT?(this[e]=t>>>8,this[e+1]=255&t):w(this,t,e,!1),e+2},r.prototype.writeInt32LE=function(t,e,n){return t=+t,e|=0,n||C(this,t,e,4,2147483647,-2147483648),r.TYPED_ARRAY_SUPPORT?(this[e]=255&t,this[e+1]=t>>>8,this[e+2]=t>>>16,this[e+3]=t>>>24):S(this,t,e,!0),e+4},r.prototype.writeInt32BE=function(t,e,n){return t=+t,e|=0,n||C(this,t,e,4,2147483647,-2147483648),t<0&&(t=4294967295+t+1),r.TYPED_ARRAY_SUPPORT?(this[e]=t>>>24,this[e+1]=t>>>16,this[e+2]=t>>>8,this[e+3]=255&t):S(this,t,e,!1),e+4},r.prototype.writeFloatLE=function(t,e,n){return x(this,t,e,!0,n)},r.prototype.writeFloatBE=function(t,e,n){return x(this,t,e,!1,n)},r.prototype.writeDoubleLE=function(t,e,n){return k(this,t,e,!0,n)},r.prototype.writeDoubleBE=function(t,e,n){return k(this,t,e,!1,n)},r.prototype.copy=function(t,e,n,o){if(n||(n=0),o||0===o||(o=this.length),e>=t.length&&(e=t.length),e||(e=0),o>0&&o=this.length)throw new RangeError("sourceStart out of bounds");if(o<0)throw new RangeError("sourceEnd out of bounds");o>this.length&&(o=this.length),t.length-e=0;--p)t[p+e]=this[p+n];else if(M<1e3||!r.TYPED_ARRAY_SUPPORT)for(p=0;p>>=0,n=void 0===n?this.length:n>>>0,t||(t=0),"number"==typeof t)for(M=e;M55295&&n<57344){if(!p){if(n>56319){(e-=3)>-1&&M.push(239,191,189);continue}if(b+1===o){(e-=3)>-1&&M.push(239,191,189);continue}p=n;continue}if(n<56320){(e-=3)>-1&&M.push(239,191,189),p=n;continue}n=65536+(p-55296<<10|n-56320)}else p&&(e-=3)>-1&&M.push(239,191,189);if(p=null,n<128){if((e-=1)<0)break;M.push(n)}else if(n<2048){if((e-=2)<0)break;M.push(n>>6|192,63&n|128)}else if(n<65536){if((e-=3)<0)break;M.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(n<1114112))throw new Error("Invalid code point");if((e-=4)<0)break;M.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return M}function U(t){return o.toByteArray(function(t){if((t=function(t){return t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")}(t).replace(I,"")).length<2)return"";for(;t.length%4!=0;)t+="=";return t}(t))}function j(t,e,n,o){for(var p=0;p=e.length||p>=t.length);++p)e[p+n]=t[p];return p}},7965:(t,e,n)=>{"use strict";var o=n(6426),p={"text/plain":"Text","text/html":"Url",default:"Text"};t.exports=function(t,e){var n,M,b,c,r,z=!1;e||(e={}),e.debug;try{if(M=o(),b=document.createRange(),c=document.getSelection(),(r=document.createElement("span")).textContent=t,r.ariaHidden="true",r.style.all="unset",r.style.position="fixed",r.style.top=0,r.style.clip="rect(0, 0, 0, 0)",r.style.whiteSpace="pre",r.style.webkitUserSelect="text",r.style.MozUserSelect="text",r.style.msUserSelect="text",r.style.userSelect="text",r.addEventListener("copy",(function(n){if(n.stopPropagation(),e.format)if(n.preventDefault(),void 0===n.clipboardData){window.clipboardData.clearData();var o=p[e.format]||p.default;window.clipboardData.setData(o,t)}else n.clipboardData.clearData(),n.clipboardData.setData(e.format,t);e.onCopy&&(n.preventDefault(),e.onCopy(n.clipboardData))})),document.body.appendChild(r),b.selectNodeContents(r),c.addRange(b),!document.execCommand("copy"))throw new Error("copy command was unsuccessful");z=!0}catch(o){try{window.clipboardData.setData(e.format||"text",t),e.onCopy&&e.onCopy(window.clipboardData),z=!0}catch(o){n=function(t){var e=(/mac os x/i.test(navigator.userAgent)?"⌘":"Ctrl")+"+C";return t.replace(/#{\s*key\s*}/g,e)}("message"in e?e.message:"Copy to clipboard: #{key}, Enter"),window.prompt(n,t)}}finally{c&&("function"==typeof c.removeRange?c.removeRange(b):c.removeAllRanges()),r&&document.body.removeChild(r),M()}return z}},251:(t,e)=>{e.read=function(t,e,n,o,p){var M,b,c=8*p-o-1,r=(1<>1,a=-7,i=n?p-1:0,O=n?-1:1,s=t[e+i];for(i+=O,M=s&(1<<-a)-1,s>>=-a,a+=c;a>0;M=256*M+t[e+i],i+=O,a-=8);for(b=M&(1<<-a)-1,M>>=-a,a+=o;a>0;b=256*b+t[e+i],i+=O,a-=8);if(0===M)M=1-z;else{if(M===r)return b?NaN:1/0*(s?-1:1);b+=Math.pow(2,o),M-=z}return(s?-1:1)*b*Math.pow(2,M-o)},e.write=function(t,e,n,o,p,M){var b,c,r,z=8*M-p-1,a=(1<>1,O=23===p?Math.pow(2,-24)-Math.pow(2,-77):0,s=o?0:M-1,A=o?1:-1,u=e<0||0===e&&1/e<0?1:0;for(e=Math.abs(e),isNaN(e)||e===1/0?(c=isNaN(e)?1:0,b=a):(b=Math.floor(Math.log(e)/Math.LN2),e*(r=Math.pow(2,-b))<1&&(b--,r*=2),(e+=b+i>=1?O/r:O*Math.pow(2,1-i))*r>=2&&(b++,r/=2),b+i>=a?(c=0,b=a):b+i>=1?(c=(e*r-1)*Math.pow(2,p),b+=i):(c=e*Math.pow(2,i-1)*Math.pow(2,p),b=0));p>=8;t[n+s]=255&c,s+=A,c/=256,p-=8);for(b=b<0;t[n+s]=255&b,s+=A,b/=256,z-=8);t[n+s-A]|=128*u}},4634:t=>{var e={}.toString;t.exports=Array.isArray||function(t){return"[object Array]"==e.call(t)}},4692:function(t,e){var n;!function(e,n){"use strict";"object"==typeof t.exports?t.exports=e.document?n(e,!0):function(t){if(!t.document)throw new Error("jQuery requires a window with a document");return n(t)}:n(e)}("undefined"!=typeof window?window:this,(function(o,p){"use strict";var M=[],b=Object.getPrototypeOf,c=M.slice,r=M.flat?function(t){return M.flat.call(t)}:function(t){return M.concat.apply([],t)},z=M.push,a=M.indexOf,i={},O=i.toString,s=i.hasOwnProperty,A=s.toString,u=A.call(Object),l={},d=function(t){return"function"==typeof t&&"number"!=typeof t.nodeType&&"function"!=typeof t.item},f=function(t){return null!=t&&t===t.window},q=o.document,h={type:!0,src:!0,nonce:!0,noModule:!0};function W(t,e,n){var o,p,M=(n=n||q).createElement("script");if(M.text=t,e)for(o in h)(p=e[o]||e.getAttribute&&e.getAttribute(o))&&M.setAttribute(o,p);n.head.appendChild(M).parentNode.removeChild(M)}function v(t){return null==t?t+"":"object"==typeof t||"function"==typeof t?i[O.call(t)]||"object":typeof t}var R="3.7.1",m=/HTML$/i,g=function(t,e){return new g.fn.init(t,e)};function L(t){var e=!!t&&"length"in t&&t.length,n=v(t);return!d(t)&&!f(t)&&("array"===n||0===e||"number"==typeof e&&e>0&&e-1 in t)}function y(t,e){return t.nodeName&&t.nodeName.toLowerCase()===e.toLowerCase()}g.fn=g.prototype={jquery:R,constructor:g,length:0,toArray:function(){return c.call(this)},get:function(t){return null==t?c.call(this):t<0?this[t+this.length]:this[t]},pushStack:function(t){var e=g.merge(this.constructor(),t);return e.prevObject=this,e},each:function(t){return g.each(this,t)},map:function(t){return this.pushStack(g.map(this,(function(e,n){return t.call(e,n,e)})))},slice:function(){return this.pushStack(c.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(g.grep(this,(function(t,e){return(e+1)%2})))},odd:function(){return this.pushStack(g.grep(this,(function(t,e){return e%2})))},eq:function(t){var e=this.length,n=+t+(t<0?e:0);return this.pushStack(n>=0&&n+~]|"+T+")"+T+"*"),P=new RegExp(T+"|>"),U=new RegExp(x),j=new RegExp("^"+C+"$"),F={ID:new RegExp("^#("+C+")"),CLASS:new RegExp("^\\.("+C+")"),TAG:new RegExp("^("+C+"|[*])"),ATTR:new RegExp("^"+w),PSEUDO:new RegExp("^"+x),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+T+"*(even|odd|(([+-]|)(\\d*)n|)"+T+"*(?:([+-]|)"+T+"*(\\d+)|))"+T+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+T+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+T+"*((?:-\\d)?\\d*)"+T+"*\\)|)(?=[^-]|$)","i")},H=/^(?:input|select|textarea|button)$/i,G=/^h\d$/i,Y=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,V=new RegExp("\\\\[\\da-fA-F]{1,6}"+T+"?|\\\\([^\\r\\n\\f])","g"),K=function(t,e){var n="0x"+t.slice(1)-65536;return e||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},Q=function(){rt()},J=Ot((function(t){return!0===t.disabled&&y(t,"fieldset")}),{dir:"parentNode",next:"legend"});try{u.apply(M=c.call(S.childNodes),S.childNodes),M[S.childNodes.length].nodeType}catch(t){u={apply:function(t,e){X.apply(t,c.call(e))},call:function(t){X.apply(t,c.call(arguments,1))}}}function Z(t,e,n,o){var p,M,b,c,z,a,s,A=e&&e.ownerDocument,f=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==f&&9!==f&&11!==f)return n;if(!o&&(rt(e),e=e||r,i)){if(11!==f&&(z=Y.exec(t)))if(p=z[1]){if(9===f){if(!(b=e.getElementById(p)))return n;if(b.id===p)return u.call(n,b),n}else if(A&&(b=A.getElementById(p))&&Z.contains(e,b)&&b.id===p)return u.call(n,b),n}else{if(z[2])return u.apply(n,e.getElementsByTagName(t)),n;if((p=z[3])&&e.getElementsByClassName)return u.apply(n,e.getElementsByClassName(p)),n}if(!(R[t+" "]||O&&O.test(t))){if(s=t,A=e,1===f&&(P.test(t)||D.test(t))){for((A=$.test(t)&&ct(e.parentNode)||e)==e&&l.scope||((c=e.getAttribute("id"))?c=g.escapeSelector(c):e.setAttribute("id",c=d)),M=(a=at(t)).length;M--;)a[M]=(c?"#"+c:":scope")+" "+it(a[M]);s=a.join(",")}try{return u.apply(n,A.querySelectorAll(s)),n}catch(e){R(t,!0)}finally{c===d&&e.removeAttribute("id")}}}return ft(t.replace(B,"$1"),e,n,o)}function tt(){var t=[];return function n(o,p){return t.push(o+" ")>e.cacheLength&&delete n[t.shift()],n[o+" "]=p}}function et(t){return t[d]=!0,t}function nt(t){var e=r.createElement("fieldset");try{return!!t(e)}catch(t){return!1}finally{e.parentNode&&e.parentNode.removeChild(e),e=null}}function ot(t){return function(e){return y(e,"input")&&e.type===t}}function pt(t){return function(e){return(y(e,"input")||y(e,"button"))&&e.type===t}}function Mt(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&J(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function bt(t){return et((function(e){return e=+e,et((function(n,o){for(var p,M=t([],n.length,e),b=M.length;b--;)n[p=M[b]]&&(n[p]=!(o[p]=n[p]))}))}))}function ct(t){return t&&void 0!==t.getElementsByTagName&&t}function rt(t){var n,o=t?t.ownerDocument||t:S;return o!=r&&9===o.nodeType&&o.documentElement?(z=(r=o).documentElement,i=!g.isXMLDoc(r),A=z.matches||z.webkitMatchesSelector||z.msMatchesSelector,z.msMatchesSelector&&S!=r&&(n=r.defaultView)&&n.top!==n&&n.addEventListener("unload",Q),l.getById=nt((function(t){return z.appendChild(t).id=g.expando,!r.getElementsByName||!r.getElementsByName(g.expando).length})),l.disconnectedMatch=nt((function(t){return A.call(t,"*")})),l.scope=nt((function(){return r.querySelectorAll(":scope")})),l.cssHas=nt((function(){try{return r.querySelector(":has(*,:jqfake)"),!1}catch(t){return!0}})),l.getById?(e.filter.ID=function(t){var e=t.replace(V,K);return function(t){return t.getAttribute("id")===e}},e.find.ID=function(t,e){if(void 0!==e.getElementById&&i){var n=e.getElementById(t);return n?[n]:[]}}):(e.filter.ID=function(t){var e=t.replace(V,K);return function(t){var n=void 0!==t.getAttributeNode&&t.getAttributeNode("id");return n&&n.value===e}},e.find.ID=function(t,e){if(void 0!==e.getElementById&&i){var n,o,p,M=e.getElementById(t);if(M){if((n=M.getAttributeNode("id"))&&n.value===t)return[M];for(p=e.getElementsByName(t),o=0;M=p[o++];)if((n=M.getAttributeNode("id"))&&n.value===t)return[M]}return[]}}),e.find.TAG=function(t,e){return void 0!==e.getElementsByTagName?e.getElementsByTagName(t):e.querySelectorAll(t)},e.find.CLASS=function(t,e){if(void 0!==e.getElementsByClassName&&i)return e.getElementsByClassName(t)},O=[],nt((function(t){var e;z.appendChild(t).innerHTML="",t.querySelectorAll("[selected]").length||O.push("\\["+T+"*(?:value|"+L+")"),t.querySelectorAll("[id~="+d+"-]").length||O.push("~="),t.querySelectorAll("a#"+d+"+*").length||O.push(".#.+[+~]"),t.querySelectorAll(":checked").length||O.push(":checked"),(e=r.createElement("input")).setAttribute("type","hidden"),t.appendChild(e).setAttribute("name","D"),z.appendChild(t).disabled=!0,2!==t.querySelectorAll(":disabled").length&&O.push(":enabled",":disabled"),(e=r.createElement("input")).setAttribute("name",""),t.appendChild(e),t.querySelectorAll("[name='']").length||O.push("\\["+T+"*name"+T+"*="+T+"*(?:''|\"\")")})),l.cssHas||O.push(":has"),O=O.length&&new RegExp(O.join("|")),m=function(t,e){if(t===e)return b=!0,0;var n=!t.compareDocumentPosition-!e.compareDocumentPosition;return n||(1&(n=(t.ownerDocument||t)==(e.ownerDocument||e)?t.compareDocumentPosition(e):1)||!l.sortDetached&&e.compareDocumentPosition(t)===n?t===r||t.ownerDocument==S&&Z.contains(S,t)?-1:e===r||e.ownerDocument==S&&Z.contains(S,e)?1:p?a.call(p,t)-a.call(p,e):0:4&n?-1:1)},r):r}for(t in Z.matches=function(t,e){return Z(t,null,null,e)},Z.matchesSelector=function(t,e){if(rt(t),i&&!R[e+" "]&&(!O||!O.test(e)))try{var n=A.call(t,e);if(n||l.disconnectedMatch||t.document&&11!==t.document.nodeType)return n}catch(t){R(e,!0)}return Z(e,r,null,[t]).length>0},Z.contains=function(t,e){return(t.ownerDocument||t)!=r&&rt(t),g.contains(t,e)},Z.attr=function(t,n){(t.ownerDocument||t)!=r&&rt(t);var o=e.attrHandle[n.toLowerCase()],p=o&&s.call(e.attrHandle,n.toLowerCase())?o(t,n,!i):void 0;return void 0!==p?p:t.getAttribute(n)},Z.error=function(t){throw new Error("Syntax error, unrecognized expression: "+t)},g.uniqueSort=function(t){var e,n=[],o=0,M=0;if(b=!l.sortStable,p=!l.sortStable&&c.call(t,0),N.call(t,m),b){for(;e=t[M++];)e===t[M]&&(o=n.push(M));for(;o--;)E.call(t,n[o],1)}return p=null,t},g.fn.uniqueSort=function(){return this.pushStack(g.uniqueSort(c.apply(this)))},e=g.expr={cacheLength:50,createPseudo:et,match:F,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(t){return t[1]=t[1].replace(V,K),t[3]=(t[3]||t[4]||t[5]||"").replace(V,K),"~="===t[2]&&(t[3]=" "+t[3]+" "),t.slice(0,4)},CHILD:function(t){return t[1]=t[1].toLowerCase(),"nth"===t[1].slice(0,3)?(t[3]||Z.error(t[0]),t[4]=+(t[4]?t[5]+(t[6]||1):2*("even"===t[3]||"odd"===t[3])),t[5]=+(t[7]+t[8]||"odd"===t[3])):t[3]&&Z.error(t[0]),t},PSEUDO:function(t){var e,n=!t[6]&&t[2];return F.CHILD.test(t[0])?null:(t[3]?t[2]=t[4]||t[5]||"":n&&U.test(n)&&(e=at(n,!0))&&(e=n.indexOf(")",n.length-e)-n.length)&&(t[0]=t[0].slice(0,e),t[2]=n.slice(0,e)),t.slice(0,3))}},filter:{TAG:function(t){var e=t.replace(V,K).toLowerCase();return"*"===t?function(){return!0}:function(t){return y(t,e)}},CLASS:function(t){var e=h[t+" "];return e||(e=new RegExp("(^|"+T+")"+t+"("+T+"|$)"))&&h(t,(function(t){return e.test("string"==typeof t.className&&t.className||void 0!==t.getAttribute&&t.getAttribute("class")||"")}))},ATTR:function(t,e,n){return function(o){var p=Z.attr(o,t);return null==p?"!="===e:!e||(p+="","="===e?p===n:"!="===e?p!==n:"^="===e?n&&0===p.indexOf(n):"*="===e?n&&p.indexOf(n)>-1:"$="===e?n&&p.slice(-n.length)===n:"~="===e?(" "+p.replace(k," ")+" ").indexOf(n)>-1:"|="===e&&(p===n||p.slice(0,n.length+1)===n+"-"))}},CHILD:function(t,e,n,o,p){var M="nth"!==t.slice(0,3),b="last"!==t.slice(-4),c="of-type"===e;return 1===o&&0===p?function(t){return!!t.parentNode}:function(e,n,r){var z,a,i,O,s,A=M!==b?"nextSibling":"previousSibling",u=e.parentNode,l=c&&e.nodeName.toLowerCase(),q=!r&&!c,h=!1;if(u){if(M){for(;A;){for(i=e;i=i[A];)if(c?y(i,l):1===i.nodeType)return!1;s=A="only"===t&&!s&&"nextSibling"}return!0}if(s=[b?u.firstChild:u.lastChild],b&&q){for(h=(O=(z=(a=u[d]||(u[d]={}))[t]||[])[0]===f&&z[1])&&z[2],i=O&&u.childNodes[O];i=++O&&i&&i[A]||(h=O=0)||s.pop();)if(1===i.nodeType&&++h&&i===e){a[t]=[f,O,h];break}}else if(q&&(h=O=(z=(a=e[d]||(e[d]={}))[t]||[])[0]===f&&z[1]),!1===h)for(;(i=++O&&i&&i[A]||(h=O=0)||s.pop())&&(!(c?y(i,l):1===i.nodeType)||!++h||(q&&((a=i[d]||(i[d]={}))[t]=[f,h]),i!==e)););return(h-=p)===o||h%o==0&&h/o>=0}}},PSEUDO:function(t,n){var o,p=e.pseudos[t]||e.setFilters[t.toLowerCase()]||Z.error("unsupported pseudo: "+t);return p[d]?p(n):p.length>1?(o=[t,t,"",n],e.setFilters.hasOwnProperty(t.toLowerCase())?et((function(t,e){for(var o,M=p(t,n),b=M.length;b--;)t[o=a.call(t,M[b])]=!(e[o]=M[b])})):function(t){return p(t,0,o)}):p}},pseudos:{not:et((function(t){var e=[],n=[],o=dt(t.replace(B,"$1"));return o[d]?et((function(t,e,n,p){for(var M,b=o(t,null,p,[]),c=t.length;c--;)(M=b[c])&&(t[c]=!(e[c]=M))})):function(t,p,M){return e[0]=t,o(e,null,M,n),e[0]=null,!n.pop()}})),has:et((function(t){return function(e){return Z(t,e).length>0}})),contains:et((function(t){return t=t.replace(V,K),function(e){return(e.textContent||g.text(e)).indexOf(t)>-1}})),lang:et((function(t){return j.test(t||"")||Z.error("unsupported lang: "+t),t=t.replace(V,K).toLowerCase(),function(e){var n;do{if(n=i?e.lang:e.getAttribute("xml:lang")||e.getAttribute("lang"))return(n=n.toLowerCase())===t||0===n.indexOf(t+"-")}while((e=e.parentNode)&&1===e.nodeType);return!1}})),target:function(t){var e=o.location&&o.location.hash;return e&&e.slice(1)===t.id},root:function(t){return t===z},focus:function(t){return t===function(){try{return r.activeElement}catch(t){}}()&&r.hasFocus()&&!!(t.type||t.href||~t.tabIndex)},enabled:Mt(!1),disabled:Mt(!0),checked:function(t){return y(t,"input")&&!!t.checked||y(t,"option")&&!!t.selected},selected:function(t){return t.parentNode&&t.parentNode.selectedIndex,!0===t.selected},empty:function(t){for(t=t.firstChild;t;t=t.nextSibling)if(t.nodeType<6)return!1;return!0},parent:function(t){return!e.pseudos.empty(t)},header:function(t){return G.test(t.nodeName)},input:function(t){return H.test(t.nodeName)},button:function(t){return y(t,"input")&&"button"===t.type||y(t,"button")},text:function(t){var e;return y(t,"input")&&"text"===t.type&&(null==(e=t.getAttribute("type"))||"text"===e.toLowerCase())},first:bt((function(){return[0]})),last:bt((function(t,e){return[e-1]})),eq:bt((function(t,e,n){return[n<0?n+e:n]})),even:bt((function(t,e){for(var n=0;ne?e:n;--o>=0;)t.push(o);return t})),gt:bt((function(t,e,n){for(var o=n<0?n+e:n;++o1?function(e,n,o){for(var p=t.length;p--;)if(!t[p](e,n,o))return!1;return!0}:t[0]}function At(t,e,n,o,p){for(var M,b=[],c=0,r=t.length,z=null!=e;c-1&&(M[z]=!(b[z]=O))}}else s=At(s===b?s.splice(d,s.length):s),p?p(null,b,s,r):u.apply(b,s)}))}function lt(t){for(var o,p,M,b=t.length,c=e.relative[t[0].type],r=c||e.relative[" "],z=c?1:0,i=Ot((function(t){return t===o}),r,!0),O=Ot((function(t){return a.call(o,t)>-1}),r,!0),s=[function(t,e,p){var M=!c&&(p||e!=n)||((o=e).nodeType?i(t,e,p):O(t,e,p));return o=null,M}];z1&&st(s),z>1&&it(t.slice(0,z-1).concat({value:" "===t[z-2].type?"*":""})).replace(B,"$1"),p,z0,M=t.length>0,b=function(b,c,z,a,O){var s,A,l,d=0,q="0",h=b&&[],W=[],v=n,R=b||M&&e.find.TAG("*",O),m=f+=null==v?1:Math.random()||.1,L=R.length;for(O&&(n=c==r||c||O);q!==L&&null!=(s=R[q]);q++){if(M&&s){for(A=0,c||s.ownerDocument==r||(rt(s),z=!i);l=t[A++];)if(l(s,c||r,z)){u.call(a,s);break}O&&(f=m)}p&&((s=!l&&s)&&d--,b&&h.push(s))}if(d+=q,p&&q!==d){for(A=0;l=o[A++];)l(h,W,c,z);if(b){if(d>0)for(;q--;)h[q]||W[q]||(W[q]=_.call(a));W=At(W)}u.apply(a,W),O&&!b&&W.length>0&&d+o.length>1&&g.uniqueSort(a)}return O&&(f=m,n=v),h};return p?et(b):b}(b,M)),c.selector=t}return c}function ft(t,n,o,p){var M,b,c,r,z,a="function"==typeof t&&t,O=!p&&at(t=a.selector||t);if(o=o||[],1===O.length){if((b=O[0]=O[0].slice(0)).length>2&&"ID"===(c=b[0]).type&&9===n.nodeType&&i&&e.relative[b[1].type]){if(!(n=(e.find.ID(c.matches[0].replace(V,K),n)||[])[0]))return o;a&&(n=n.parentNode),t=t.slice(b.shift().value.length)}for(M=F.needsContext.test(t)?0:b.length;M--&&(c=b[M],!e.relative[r=c.type]);)if((z=e.find[r])&&(p=z(c.matches[0].replace(V,K),$.test(b[0].type)&&ct(n.parentNode)||n))){if(b.splice(M,1),!(t=p.length&&it(b)))return u.apply(o,p),o;break}}return(a||dt(t,O))(p,n,!i,o,!n||$.test(t)&&ct(n.parentNode)||n),o}zt.prototype=e.filters=e.pseudos,e.setFilters=new zt,l.sortStable=d.split("").sort(m).join("")===d,rt(),l.sortDetached=nt((function(t){return 1&t.compareDocumentPosition(r.createElement("fieldset"))})),g.find=Z,g.expr[":"]=g.expr.pseudos,g.unique=g.uniqueSort,Z.compile=dt,Z.select=ft,Z.setDocument=rt,Z.tokenize=at,Z.escape=g.escapeSelector,Z.getText=g.text,Z.isXML=g.isXMLDoc,Z.selectors=g.expr,Z.support=g.support,Z.uniqueSort=g.uniqueSort}();var x=function(t,e,n){for(var o=[],p=void 0!==n;(t=t[e])&&9!==t.nodeType;)if(1===t.nodeType){if(p&&g(t).is(n))break;o.push(t)}return o},k=function(t,e){for(var n=[];t;t=t.nextSibling)1===t.nodeType&&t!==e&&n.push(t);return n},I=g.expr.match.needsContext,D=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function P(t,e,n){return d(e)?g.grep(t,(function(t,o){return!!e.call(t,o,t)!==n})):e.nodeType?g.grep(t,(function(t){return t===e!==n})):"string"!=typeof e?g.grep(t,(function(t){return a.call(e,t)>-1!==n})):g.filter(e,t,n)}g.filter=function(t,e,n){var o=e[0];return n&&(t=":not("+t+")"),1===e.length&&1===o.nodeType?g.find.matchesSelector(o,t)?[o]:[]:g.find.matches(t,g.grep(e,(function(t){return 1===t.nodeType})))},g.fn.extend({find:function(t){var e,n,o=this.length,p=this;if("string"!=typeof t)return this.pushStack(g(t).filter((function(){for(e=0;e1?g.uniqueSort(n):n},filter:function(t){return this.pushStack(P(this,t||[],!1))},not:function(t){return this.pushStack(P(this,t||[],!0))},is:function(t){return!!P(this,"string"==typeof t&&I.test(t)?g(t):t||[],!1).length}});var U,j=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(g.fn.init=function(t,e,n){var o,p;if(!t)return this;if(n=n||U,"string"==typeof t){if(!(o="<"===t[0]&&">"===t[t.length-1]&&t.length>=3?[null,t,null]:j.exec(t))||!o[1]&&e)return!e||e.jquery?(e||n).find(t):this.constructor(e).find(t);if(o[1]){if(e=e instanceof g?e[0]:e,g.merge(this,g.parseHTML(o[1],e&&e.nodeType?e.ownerDocument||e:q,!0)),D.test(o[1])&&g.isPlainObject(e))for(o in e)d(this[o])?this[o](e[o]):this.attr(o,e[o]);return this}return(p=q.getElementById(o[2]))&&(this[0]=p,this.length=1),this}return t.nodeType?(this[0]=t,this.length=1,this):d(t)?void 0!==n.ready?n.ready(t):t(g):g.makeArray(t,this)}).prototype=g.fn,U=g(q);var F=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function G(t,e){for(;(t=t[e])&&1!==t.nodeType;);return t}g.fn.extend({has:function(t){var e=g(t,this),n=e.length;return this.filter((function(){for(var t=0;t-1:1===n.nodeType&&g.find.matchesSelector(n,t))){M.push(n);break}return this.pushStack(M.length>1?g.uniqueSort(M):M)},index:function(t){return t?"string"==typeof t?a.call(g(t),this[0]):a.call(this,t.jquery?t[0]:t):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(t,e){return this.pushStack(g.uniqueSort(g.merge(this.get(),g(t,e))))},addBack:function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}}),g.each({parent:function(t){var e=t.parentNode;return e&&11!==e.nodeType?e:null},parents:function(t){return x(t,"parentNode")},parentsUntil:function(t,e,n){return x(t,"parentNode",n)},next:function(t){return G(t,"nextSibling")},prev:function(t){return G(t,"previousSibling")},nextAll:function(t){return x(t,"nextSibling")},prevAll:function(t){return x(t,"previousSibling")},nextUntil:function(t,e,n){return x(t,"nextSibling",n)},prevUntil:function(t,e,n){return x(t,"previousSibling",n)},siblings:function(t){return k((t.parentNode||{}).firstChild,t)},children:function(t){return k(t.firstChild)},contents:function(t){return null!=t.contentDocument&&b(t.contentDocument)?t.contentDocument:(y(t,"template")&&(t=t.content||t),g.merge([],t.childNodes))}},(function(t,e){g.fn[t]=function(n,o){var p=g.map(this,e,n);return"Until"!==t.slice(-5)&&(o=n),o&&"string"==typeof o&&(p=g.filter(o,p)),this.length>1&&(H[t]||g.uniqueSort(p),F.test(t)&&p.reverse()),this.pushStack(p)}}));var Y=/[^\x20\t\r\n\f]+/g;function $(t){return t}function V(t){throw t}function K(t,e,n,o){var p;try{t&&d(p=t.promise)?p.call(t).done(e).fail(n):t&&d(p=t.then)?p.call(t,e,n):e.apply(void 0,[t].slice(o))}catch(t){n.apply(void 0,[t])}}g.Callbacks=function(t){t="string"==typeof t?function(t){var e={};return g.each(t.match(Y)||[],(function(t,n){e[n]=!0})),e}(t):g.extend({},t);var e,n,o,p,M=[],b=[],c=-1,r=function(){for(p=p||t.once,o=e=!0;b.length;c=-1)for(n=b.shift();++c-1;)M.splice(n,1),n<=c&&c--})),this},has:function(t){return t?g.inArray(t,M)>-1:M.length>0},empty:function(){return M&&(M=[]),this},disable:function(){return p=b=[],M=n="",this},disabled:function(){return!M},lock:function(){return p=b=[],n||e||(M=n=""),this},locked:function(){return!!p},fireWith:function(t,n){return p||(n=[t,(n=n||[]).slice?n.slice():n],b.push(n),e||r()),this},fire:function(){return z.fireWith(this,arguments),this},fired:function(){return!!o}};return z},g.extend({Deferred:function(t){var e=[["notify","progress",g.Callbacks("memory"),g.Callbacks("memory"),2],["resolve","done",g.Callbacks("once memory"),g.Callbacks("once memory"),0,"resolved"],["reject","fail",g.Callbacks("once memory"),g.Callbacks("once memory"),1,"rejected"]],n="pending",p={state:function(){return n},always:function(){return M.done(arguments).fail(arguments),this},catch:function(t){return p.then(null,t)},pipe:function(){var t=arguments;return g.Deferred((function(n){g.each(e,(function(e,o){var p=d(t[o[4]])&&t[o[4]];M[o[1]]((function(){var t=p&&p.apply(this,arguments);t&&d(t.promise)?t.promise().progress(n.notify).done(n.resolve).fail(n.reject):n[o[0]+"With"](this,p?[t]:arguments)}))})),t=null})).promise()},then:function(t,n,p){var M=0;function b(t,e,n,p){return function(){var c=this,r=arguments,z=function(){var o,z;if(!(t=M&&(n!==V&&(c=void 0,r=[o]),e.rejectWith(c,r))}};t?a():(g.Deferred.getErrorHook?a.error=g.Deferred.getErrorHook():g.Deferred.getStackHook&&(a.error=g.Deferred.getStackHook()),o.setTimeout(a))}}return g.Deferred((function(o){e[0][3].add(b(0,o,d(p)?p:$,o.notifyWith)),e[1][3].add(b(0,o,d(t)?t:$)),e[2][3].add(b(0,o,d(n)?n:V))})).promise()},promise:function(t){return null!=t?g.extend(t,p):p}},M={};return g.each(e,(function(t,o){var b=o[2],c=o[5];p[o[1]]=b.add,c&&b.add((function(){n=c}),e[3-t][2].disable,e[3-t][3].disable,e[0][2].lock,e[0][3].lock),b.add(o[3].fire),M[o[0]]=function(){return M[o[0]+"With"](this===M?void 0:this,arguments),this},M[o[0]+"With"]=b.fireWith})),p.promise(M),t&&t.call(M,M),M},when:function(t){var e=arguments.length,n=e,o=Array(n),p=c.call(arguments),M=g.Deferred(),b=function(t){return function(n){o[t]=this,p[t]=arguments.length>1?c.call(arguments):n,--e||M.resolveWith(o,p)}};if(e<=1&&(K(t,M.done(b(n)).resolve,M.reject,!e),"pending"===M.state()||d(p[n]&&p[n].then)))return M.then();for(;n--;)K(p[n],b(n),M.reject);return M.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;g.Deferred.exceptionHook=function(t,e){o.console&&o.console.warn&&t&&Q.test(t.name)&&o.console.warn("jQuery.Deferred exception: "+t.message,t.stack,e)},g.readyException=function(t){o.setTimeout((function(){throw t}))};var J=g.Deferred();function Z(){q.removeEventListener("DOMContentLoaded",Z),o.removeEventListener("load",Z),g.ready()}g.fn.ready=function(t){return J.then(t).catch((function(t){g.readyException(t)})),this},g.extend({isReady:!1,readyWait:1,ready:function(t){(!0===t?--g.readyWait:g.isReady)||(g.isReady=!0,!0!==t&&--g.readyWait>0||J.resolveWith(q,[g]))}}),g.ready.then=J.then,"complete"===q.readyState||"loading"!==q.readyState&&!q.documentElement.doScroll?o.setTimeout(g.ready):(q.addEventListener("DOMContentLoaded",Z),o.addEventListener("load",Z));var tt=function(t,e,n,o,p,M,b){var c=0,r=t.length,z=null==n;if("object"===v(n))for(c in p=!0,n)tt(t,e,c,n[c],!0,M,b);else if(void 0!==o&&(p=!0,d(o)||(b=!0),z&&(b?(e.call(t,o),e=null):(z=e,e=function(t,e,n){return z.call(g(t),n)})),e))for(;c1,null,!0)},removeData:function(t){return this.each((function(){rt.remove(this,t)}))}}),g.extend({queue:function(t,e,n){var o;if(t)return e=(e||"fx")+"queue",o=ct.get(t,e),n&&(!o||Array.isArray(n)?o=ct.access(t,e,g.makeArray(n)):o.push(n)),o||[]},dequeue:function(t,e){e=e||"fx";var n=g.queue(t,e),o=n.length,p=n.shift(),M=g._queueHooks(t,e);"inprogress"===p&&(p=n.shift(),o--),p&&("fx"===e&&n.unshift("inprogress"),delete M.stop,p.call(t,(function(){g.dequeue(t,e)}),M)),!o&&M&&M.empty.fire()},_queueHooks:function(t,e){var n=e+"queueHooks";return ct.get(t,n)||ct.access(t,n,{empty:g.Callbacks("once memory").add((function(){ct.remove(t,[e+"queue",n])}))})}}),g.fn.extend({queue:function(t,e){var n=2;return"string"!=typeof t&&(e=t,t="fx",n--),arguments.length\x20\t\r\n\f]*)/i,yt=/^$|^module$|\/(?:java|ecma)script/i;Rt=q.createDocumentFragment().appendChild(q.createElement("div")),(mt=q.createElement("input")).setAttribute("type","radio"),mt.setAttribute("checked","checked"),mt.setAttribute("name","t"),Rt.appendChild(mt),l.checkClone=Rt.cloneNode(!0).cloneNode(!0).lastChild.checked,Rt.innerHTML="",l.noCloneChecked=!!Rt.cloneNode(!0).lastChild.defaultValue,Rt.innerHTML="",l.option=!!Rt.lastChild;var _t={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function Nt(t,e){var n;return n=void 0!==t.getElementsByTagName?t.getElementsByTagName(e||"*"):void 0!==t.querySelectorAll?t.querySelectorAll(e||"*"):[],void 0===e||e&&y(t,e)?g.merge([t],n):n}function Et(t,e){for(var n=0,o=t.length;n",""]);var Tt=/<|&#?\w+;/;function Bt(t,e,n,o,p){for(var M,b,c,r,z,a,i=e.createDocumentFragment(),O=[],s=0,A=t.length;s-1)p&&p.push(M);else if(z=lt(M),b=Nt(i.appendChild(M),"script"),z&&Et(b),n)for(a=0;M=b[a++];)yt.test(M.type||"")&&n.push(M);return i}var Ct=/^([^.]*)(?:\.(.+)|)/;function wt(){return!0}function St(){return!1}function Xt(t,e,n,o,p,M){var b,c;if("object"==typeof e){for(c in"string"!=typeof n&&(o=o||n,n=void 0),e)Xt(t,c,n,o,e[c],M);return t}if(null==o&&null==p?(p=n,o=n=void 0):null==p&&("string"==typeof n?(p=o,o=void 0):(p=o,o=n,n=void 0)),!1===p)p=St;else if(!p)return t;return 1===M&&(b=p,p=function(t){return g().off(t),b.apply(this,arguments)},p.guid=b.guid||(b.guid=g.guid++)),t.each((function(){g.event.add(this,e,p,o,n)}))}function xt(t,e,n){n?(ct.set(t,e,!1),g.event.add(t,e,{namespace:!1,handler:function(t){var n,o=ct.get(this,e);if(1&t.isTrigger&&this[e]){if(o)(g.event.special[e]||{}).delegateType&&t.stopPropagation();else if(o=c.call(arguments),ct.set(this,e,o),this[e](),n=ct.get(this,e),ct.set(this,e,!1),o!==n)return t.stopImmediatePropagation(),t.preventDefault(),n}else o&&(ct.set(this,e,g.event.trigger(o[0],o.slice(1),this)),t.stopPropagation(),t.isImmediatePropagationStopped=wt)}})):void 0===ct.get(t,e)&&g.event.add(t,e,wt)}g.event={global:{},add:function(t,e,n,o,p){var M,b,c,r,z,a,i,O,s,A,u,l=ct.get(t);if(Mt(t))for(n.handler&&(n=(M=n).handler,p=M.selector),p&&g.find.matchesSelector(ut,p),n.guid||(n.guid=g.guid++),(r=l.events)||(r=l.events=Object.create(null)),(b=l.handle)||(b=l.handle=function(e){return void 0!==g&&g.event.triggered!==e.type?g.event.dispatch.apply(t,arguments):void 0}),z=(e=(e||"").match(Y)||[""]).length;z--;)s=u=(c=Ct.exec(e[z])||[])[1],A=(c[2]||"").split(".").sort(),s&&(i=g.event.special[s]||{},s=(p?i.delegateType:i.bindType)||s,i=g.event.special[s]||{},a=g.extend({type:s,origType:u,data:o,handler:n,guid:n.guid,selector:p,needsContext:p&&g.expr.match.needsContext.test(p),namespace:A.join(".")},M),(O=r[s])||((O=r[s]=[]).delegateCount=0,i.setup&&!1!==i.setup.call(t,o,A,b)||t.addEventListener&&t.addEventListener(s,b)),i.add&&(i.add.call(t,a),a.handler.guid||(a.handler.guid=n.guid)),p?O.splice(O.delegateCount++,0,a):O.push(a),g.event.global[s]=!0)},remove:function(t,e,n,o,p){var M,b,c,r,z,a,i,O,s,A,u,l=ct.hasData(t)&&ct.get(t);if(l&&(r=l.events)){for(z=(e=(e||"").match(Y)||[""]).length;z--;)if(s=u=(c=Ct.exec(e[z])||[])[1],A=(c[2]||"").split(".").sort(),s){for(i=g.event.special[s]||{},O=r[s=(o?i.delegateType:i.bindType)||s]||[],c=c[2]&&new RegExp("(^|\\.)"+A.join("\\.(?:.*\\.|)")+"(\\.|$)"),b=M=O.length;M--;)a=O[M],!p&&u!==a.origType||n&&n.guid!==a.guid||c&&!c.test(a.namespace)||o&&o!==a.selector&&("**"!==o||!a.selector)||(O.splice(M,1),a.selector&&O.delegateCount--,i.remove&&i.remove.call(t,a));b&&!O.length&&(i.teardown&&!1!==i.teardown.call(t,A,l.handle)||g.removeEvent(t,s,l.handle),delete r[s])}else for(s in r)g.event.remove(t,s+e[z],n,o,!0);g.isEmptyObject(r)&&ct.remove(t,"handle events")}},dispatch:function(t){var e,n,o,p,M,b,c=new Array(arguments.length),r=g.event.fix(t),z=(ct.get(this,"events")||Object.create(null))[r.type]||[],a=g.event.special[r.type]||{};for(c[0]=r,e=1;e=1))for(;z!==this;z=z.parentNode||this)if(1===z.nodeType&&("click"!==t.type||!0!==z.disabled)){for(M=[],b={},n=0;n-1:g.find(p,this,null,[z]).length),b[p]&&M.push(o);M.length&&c.push({elem:z,handlers:M})}return z=this,r\s*$/g;function Pt(t,e){return y(t,"table")&&y(11!==e.nodeType?e:e.firstChild,"tr")&&g(t).children("tbody")[0]||t}function Ut(t){return t.type=(null!==t.getAttribute("type"))+"/"+t.type,t}function jt(t){return"true/"===(t.type||"").slice(0,5)?t.type=t.type.slice(5):t.removeAttribute("type"),t}function Ft(t,e){var n,o,p,M,b,c;if(1===e.nodeType){if(ct.hasData(t)&&(c=ct.get(t).events))for(p in ct.remove(e,"handle events"),c)for(n=0,o=c[p].length;n1&&"string"==typeof A&&!l.checkClone&&It.test(A))return t.each((function(p){var M=t.eq(p);u&&(e[0]=A.call(this,p,M.html())),Gt(M,e,n,o)}));if(O&&(M=(p=Bt(e,t[0].ownerDocument,!1,t,o)).firstChild,1===p.childNodes.length&&(p=M),M||o)){for(c=(b=g.map(Nt(p,"script"),Ut)).length;i0&&Et(b,!r&&Nt(t,"script")),c},cleanData:function(t){for(var e,n,o,p=g.event.special,M=0;void 0!==(n=t[M]);M++)if(Mt(n)){if(e=n[ct.expando]){if(e.events)for(o in e.events)p[o]?g.event.remove(n,o):g.removeEvent(n,o,e.handle);n[ct.expando]=void 0}n[rt.expando]&&(n[rt.expando]=void 0)}}}),g.fn.extend({detach:function(t){return Yt(this,t,!0)},remove:function(t){return Yt(this,t)},text:function(t){return tt(this,(function(t){return void 0===t?g.text(this):this.empty().each((function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=t)}))}),null,t,arguments.length)},append:function(){return Gt(this,arguments,(function(t){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Pt(this,t).appendChild(t)}))},prepend:function(){return Gt(this,arguments,(function(t){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var e=Pt(this,t);e.insertBefore(t,e.firstChild)}}))},before:function(){return Gt(this,arguments,(function(t){this.parentNode&&this.parentNode.insertBefore(t,this)}))},after:function(){return Gt(this,arguments,(function(t){this.parentNode&&this.parentNode.insertBefore(t,this.nextSibling)}))},empty:function(){for(var t,e=0;null!=(t=this[e]);e++)1===t.nodeType&&(g.cleanData(Nt(t,!1)),t.textContent="");return this},clone:function(t,e){return t=null!=t&&t,e=null==e?t:e,this.map((function(){return g.clone(this,t,e)}))},html:function(t){return tt(this,(function(t){var e=this[0]||{},n=0,o=this.length;if(void 0===t&&1===e.nodeType)return e.innerHTML;if("string"==typeof t&&!kt.test(t)&&!_t[(Lt.exec(t)||["",""])[1].toLowerCase()]){t=g.htmlPrefilter(t);try{for(;n=0&&(r+=Math.max(0,Math.ceil(t["offset"+e[0].toUpperCase()+e.slice(1)]-M-r-c-.5))||0),r+z}function ae(t,e,n){var o=Kt(t),p=(!l.boxSizingReliable()||n)&&"border-box"===g.css(t,"boxSizing",!1,o),M=p,b=Zt(t,e,o),c="offset"+e[0].toUpperCase()+e.slice(1);if($t.test(b)){if(!n)return b;b="auto"}return(!l.boxSizingReliable()&&p||!l.reliableTrDimensions()&&y(t,"tr")||"auto"===b||!parseFloat(b)&&"inline"===g.css(t,"display",!1,o))&&t.getClientRects().length&&(p="border-box"===g.css(t,"boxSizing",!1,o),(M=c in t)&&(b=t[c])),(b=parseFloat(b)||0)+ze(t,e,n||(p?"border":"content"),M,o,b)+"px"}function ie(t,e,n,o,p){return new ie.prototype.init(t,e,n,o,p)}g.extend({cssHooks:{opacity:{get:function(t,e){if(e){var n=Zt(t,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,aspectRatio:!0,borderImageSlice:!0,columnCount:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,scale:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeMiterlimit:!0,strokeOpacity:!0},cssProps:{},style:function(t,e,n,o){if(t&&3!==t.nodeType&&8!==t.nodeType&&t.style){var p,M,b,c=pt(e),r=Vt.test(e),z=t.style;if(r||(e=pe(c)),b=g.cssHooks[e]||g.cssHooks[c],void 0===n)return b&&"get"in b&&void 0!==(p=b.get(t,!1,o))?p:z[e];"string"===(M=typeof n)&&(p=st.exec(n))&&p[1]&&(n=qt(t,e,p),M="number"),null!=n&&n==n&&("number"!==M||r||(n+=p&&p[3]||(g.cssNumber[c]?"":"px")),l.clearCloneStyle||""!==n||0!==e.indexOf("background")||(z[e]="inherit"),b&&"set"in b&&void 0===(n=b.set(t,n,o))||(r?z.setProperty(e,n):z[e]=n))}},css:function(t,e,n,o){var p,M,b,c=pt(e);return Vt.test(e)||(e=pe(c)),(b=g.cssHooks[e]||g.cssHooks[c])&&"get"in b&&(p=b.get(t,!0,n)),void 0===p&&(p=Zt(t,e,o)),"normal"===p&&e in ce&&(p=ce[e]),""===n||n?(M=parseFloat(p),!0===n||isFinite(M)?M||0:p):p}}),g.each(["height","width"],(function(t,e){g.cssHooks[e]={get:function(t,n,o){if(n)return!Me.test(g.css(t,"display"))||t.getClientRects().length&&t.getBoundingClientRect().width?ae(t,e,o):Qt(t,be,(function(){return ae(t,e,o)}))},set:function(t,n,o){var p,M=Kt(t),b=!l.scrollboxSize()&&"absolute"===M.position,c=(b||o)&&"border-box"===g.css(t,"boxSizing",!1,M),r=o?ze(t,e,o,c,M):0;return c&&b&&(r-=Math.ceil(t["offset"+e[0].toUpperCase()+e.slice(1)]-parseFloat(M[e])-ze(t,e,"border",!1,M)-.5)),r&&(p=st.exec(n))&&"px"!==(p[3]||"px")&&(t.style[e]=n,n=g.css(t,e)),re(0,n,r)}}})),g.cssHooks.marginLeft=te(l.reliableMarginLeft,(function(t,e){if(e)return(parseFloat(Zt(t,"marginLeft"))||t.getBoundingClientRect().left-Qt(t,{marginLeft:0},(function(){return t.getBoundingClientRect().left})))+"px"})),g.each({margin:"",padding:"",border:"Width"},(function(t,e){g.cssHooks[t+e]={expand:function(n){for(var o=0,p={},M="string"==typeof n?n.split(" "):[n];o<4;o++)p[t+At[o]+e]=M[o]||M[o-2]||M[0];return p}},"margin"!==t&&(g.cssHooks[t+e].set=re)})),g.fn.extend({css:function(t,e){return tt(this,(function(t,e,n){var o,p,M={},b=0;if(Array.isArray(e)){for(o=Kt(t),p=e.length;b1)}}),g.Tween=ie,ie.prototype={constructor:ie,init:function(t,e,n,o,p,M){this.elem=t,this.prop=n,this.easing=p||g.easing._default,this.options=e,this.start=this.now=this.cur(),this.end=o,this.unit=M||(g.cssNumber[n]?"":"px")},cur:function(){var t=ie.propHooks[this.prop];return t&&t.get?t.get(this):ie.propHooks._default.get(this)},run:function(t){var e,n=ie.propHooks[this.prop];return this.options.duration?this.pos=e=g.easing[this.easing](t,this.options.duration*t,0,1,this.options.duration):this.pos=e=t,this.now=(this.end-this.start)*e+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):ie.propHooks._default.set(this),this}},ie.prototype.init.prototype=ie.prototype,ie.propHooks={_default:{get:function(t){var e;return 1!==t.elem.nodeType||null!=t.elem[t.prop]&&null==t.elem.style[t.prop]?t.elem[t.prop]:(e=g.css(t.elem,t.prop,""))&&"auto"!==e?e:0},set:function(t){g.fx.step[t.prop]?g.fx.step[t.prop](t):1!==t.elem.nodeType||!g.cssHooks[t.prop]&&null==t.elem.style[pe(t.prop)]?t.elem[t.prop]=t.now:g.style(t.elem,t.prop,t.now+t.unit)}}},ie.propHooks.scrollTop=ie.propHooks.scrollLeft={set:function(t){t.elem.nodeType&&t.elem.parentNode&&(t.elem[t.prop]=t.now)}},g.easing={linear:function(t){return t},swing:function(t){return.5-Math.cos(t*Math.PI)/2},_default:"swing"},g.fx=ie.prototype.init,g.fx.step={};var Oe,se,Ae=/^(?:toggle|show|hide)$/,ue=/queueHooks$/;function le(){se&&(!1===q.hidden&&o.requestAnimationFrame?o.requestAnimationFrame(le):o.setTimeout(le,g.fx.interval),g.fx.tick())}function de(){return o.setTimeout((function(){Oe=void 0})),Oe=Date.now()}function fe(t,e){var n,o=0,p={height:t};for(e=e?1:0;o<4;o+=2-e)p["margin"+(n=At[o])]=p["padding"+n]=t;return e&&(p.opacity=p.width=t),p}function qe(t,e,n){for(var o,p=(he.tweeners[e]||[]).concat(he.tweeners["*"]),M=0,b=p.length;M1)},removeAttr:function(t){return this.each((function(){g.removeAttr(this,t)}))}}),g.extend({attr:function(t,e,n){var o,p,M=t.nodeType;if(3!==M&&8!==M&&2!==M)return void 0===t.getAttribute?g.prop(t,e,n):(1===M&&g.isXMLDoc(t)||(p=g.attrHooks[e.toLowerCase()]||(g.expr.match.bool.test(e)?We:void 0)),void 0!==n?null===n?void g.removeAttr(t,e):p&&"set"in p&&void 0!==(o=p.set(t,n,e))?o:(t.setAttribute(e,n+""),n):p&&"get"in p&&null!==(o=p.get(t,e))?o:null==(o=g.find.attr(t,e))?void 0:o)},attrHooks:{type:{set:function(t,e){if(!l.radioValue&&"radio"===e&&y(t,"input")){var n=t.value;return t.setAttribute("type",e),n&&(t.value=n),e}}}},removeAttr:function(t,e){var n,o=0,p=e&&e.match(Y);if(p&&1===t.nodeType)for(;n=p[o++];)t.removeAttribute(n)}}),We={set:function(t,e,n){return!1===e?g.removeAttr(t,n):t.setAttribute(n,n),n}},g.each(g.expr.match.bool.source.match(/\w+/g),(function(t,e){var n=ve[e]||g.find.attr;ve[e]=function(t,e,o){var p,M,b=e.toLowerCase();return o||(M=ve[b],ve[b]=p,p=null!=n(t,e,o)?b:null,ve[b]=M),p}}));var Re=/^(?:input|select|textarea|button)$/i,me=/^(?:a|area)$/i;function ge(t){return(t.match(Y)||[]).join(" ")}function Le(t){return t.getAttribute&&t.getAttribute("class")||""}function ye(t){return Array.isArray(t)?t:"string"==typeof t&&t.match(Y)||[]}g.fn.extend({prop:function(t,e){return tt(this,g.prop,t,e,arguments.length>1)},removeProp:function(t){return this.each((function(){delete this[g.propFix[t]||t]}))}}),g.extend({prop:function(t,e,n){var o,p,M=t.nodeType;if(3!==M&&8!==M&&2!==M)return 1===M&&g.isXMLDoc(t)||(e=g.propFix[e]||e,p=g.propHooks[e]),void 0!==n?p&&"set"in p&&void 0!==(o=p.set(t,n,e))?o:t[e]=n:p&&"get"in p&&null!==(o=p.get(t,e))?o:t[e]},propHooks:{tabIndex:{get:function(t){var e=g.find.attr(t,"tabindex");return e?parseInt(e,10):Re.test(t.nodeName)||me.test(t.nodeName)&&t.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),l.optSelected||(g.propHooks.selected={get:function(t){var e=t.parentNode;return e&&e.parentNode&&e.parentNode.selectedIndex,null},set:function(t){var e=t.parentNode;e&&(e.selectedIndex,e.parentNode&&e.parentNode.selectedIndex)}}),g.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],(function(){g.propFix[this.toLowerCase()]=this})),g.fn.extend({addClass:function(t){var e,n,o,p,M,b;return d(t)?this.each((function(e){g(this).addClass(t.call(this,e,Le(this)))})):(e=ye(t)).length?this.each((function(){if(o=Le(this),n=1===this.nodeType&&" "+ge(o)+" "){for(M=0;M-1;)n=n.replace(" "+p+" "," ");b=ge(n),o!==b&&this.setAttribute("class",b)}})):this:this.attr("class","")},toggleClass:function(t,e){var n,o,p,M,b=typeof t,c="string"===b||Array.isArray(t);return d(t)?this.each((function(n){g(this).toggleClass(t.call(this,n,Le(this),e),e)})):"boolean"==typeof e&&c?e?this.addClass(t):this.removeClass(t):(n=ye(t),this.each((function(){if(c)for(M=g(this),p=0;p-1)return!0;return!1}});var _e=/\r/g;g.fn.extend({val:function(t){var e,n,o,p=this[0];return arguments.length?(o=d(t),this.each((function(n){var p;1===this.nodeType&&(null==(p=o?t.call(this,n,g(this).val()):t)?p="":"number"==typeof p?p+="":Array.isArray(p)&&(p=g.map(p,(function(t){return null==t?"":t+""}))),(e=g.valHooks[this.type]||g.valHooks[this.nodeName.toLowerCase()])&&"set"in e&&void 0!==e.set(this,p,"value")||(this.value=p))}))):p?(e=g.valHooks[p.type]||g.valHooks[p.nodeName.toLowerCase()])&&"get"in e&&void 0!==(n=e.get(p,"value"))?n:"string"==typeof(n=p.value)?n.replace(_e,""):null==n?"":n:void 0}}),g.extend({valHooks:{option:{get:function(t){var e=g.find.attr(t,"value");return null!=e?e:ge(g.text(t))}},select:{get:function(t){var e,n,o,p=t.options,M=t.selectedIndex,b="select-one"===t.type,c=b?null:[],r=b?M+1:p.length;for(o=M<0?r:b?M:0;o-1)&&(n=!0);return n||(t.selectedIndex=-1),M}}}}),g.each(["radio","checkbox"],(function(){g.valHooks[this]={set:function(t,e){if(Array.isArray(e))return t.checked=g.inArray(g(t).val(),e)>-1}},l.checkOn||(g.valHooks[this].get=function(t){return null===t.getAttribute("value")?"on":t.value})}));var Ne=o.location,Ee={guid:Date.now()},Te=/\?/;g.parseXML=function(t){var e,n;if(!t||"string"!=typeof t)return null;try{e=(new o.DOMParser).parseFromString(t,"text/xml")}catch(t){}return n=e&&e.getElementsByTagName("parsererror")[0],e&&!n||g.error("Invalid XML: "+(n?g.map(n.childNodes,(function(t){return t.textContent})).join("\n"):t)),e};var Be=/^(?:focusinfocus|focusoutblur)$/,Ce=function(t){t.stopPropagation()};g.extend(g.event,{trigger:function(t,e,n,p){var M,b,c,r,z,a,i,O,A=[n||q],u=s.call(t,"type")?t.type:t,l=s.call(t,"namespace")?t.namespace.split("."):[];if(b=O=c=n=n||q,3!==n.nodeType&&8!==n.nodeType&&!Be.test(u+g.event.triggered)&&(u.indexOf(".")>-1&&(l=u.split("."),u=l.shift(),l.sort()),z=u.indexOf(":")<0&&"on"+u,(t=t[g.expando]?t:new g.Event(u,"object"==typeof t&&t)).isTrigger=p?2:3,t.namespace=l.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+l.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=n),e=null==e?[t]:g.makeArray(e,[t]),i=g.event.special[u]||{},p||!i.trigger||!1!==i.trigger.apply(n,e))){if(!p&&!i.noBubble&&!f(n)){for(r=i.delegateType||u,Be.test(r+u)||(b=b.parentNode);b;b=b.parentNode)A.push(b),c=b;c===(n.ownerDocument||q)&&A.push(c.defaultView||c.parentWindow||o)}for(M=0;(b=A[M++])&&!t.isPropagationStopped();)O=b,t.type=M>1?r:i.bindType||u,(a=(ct.get(b,"events")||Object.create(null))[t.type]&&ct.get(b,"handle"))&&a.apply(b,e),(a=z&&b[z])&&a.apply&&Mt(b)&&(t.result=a.apply(b,e),!1===t.result&&t.preventDefault());return t.type=u,p||t.isDefaultPrevented()||i._default&&!1!==i._default.apply(A.pop(),e)||!Mt(n)||z&&d(n[u])&&!f(n)&&((c=n[z])&&(n[z]=null),g.event.triggered=u,t.isPropagationStopped()&&O.addEventListener(u,Ce),n[u](),t.isPropagationStopped()&&O.removeEventListener(u,Ce),g.event.triggered=void 0,c&&(n[z]=c)),t.result}},simulate:function(t,e,n){var o=g.extend(new g.Event,n,{type:t,isSimulated:!0});g.event.trigger(o,null,e)}}),g.fn.extend({trigger:function(t,e){return this.each((function(){g.event.trigger(t,e,this)}))},triggerHandler:function(t,e){var n=this[0];if(n)return g.event.trigger(t,e,n,!0)}});var we=/\[\]$/,Se=/\r?\n/g,Xe=/^(?:submit|button|image|reset|file)$/i,xe=/^(?:input|select|textarea|keygen)/i;function ke(t,e,n,o){var p;if(Array.isArray(e))g.each(e,(function(e,p){n||we.test(t)?o(t,p):ke(t+"["+("object"==typeof p&&null!=p?e:"")+"]",p,n,o)}));else if(n||"object"!==v(e))o(t,e);else for(p in e)ke(t+"["+p+"]",e[p],n,o)}g.param=function(t,e){var n,o=[],p=function(t,e){var n=d(e)?e():e;o[o.length]=encodeURIComponent(t)+"="+encodeURIComponent(null==n?"":n)};if(null==t)return"";if(Array.isArray(t)||t.jquery&&!g.isPlainObject(t))g.each(t,(function(){p(this.name,this.value)}));else for(n in t)ke(n,t[n],e,p);return o.join("&")},g.fn.extend({serialize:function(){return g.param(this.serializeArray())},serializeArray:function(){return this.map((function(){var t=g.prop(this,"elements");return t?g.makeArray(t):this})).filter((function(){var t=this.type;return this.name&&!g(this).is(":disabled")&&xe.test(this.nodeName)&&!Xe.test(t)&&(this.checked||!gt.test(t))})).map((function(t,e){var n=g(this).val();return null==n?null:Array.isArray(n)?g.map(n,(function(t){return{name:e.name,value:t.replace(Se,"\r\n")}})):{name:e.name,value:n.replace(Se,"\r\n")}})).get()}});var Ie=/%20/g,De=/#.*$/,Pe=/([?&])_=[^&]*/,Ue=/^(.*?):[ \t]*([^\r\n]*)$/gm,je=/^(?:GET|HEAD)$/,Fe=/^\/\//,He={},Ge={},Ye="*/".concat("*"),$e=q.createElement("a");function Ve(t){return function(e,n){"string"!=typeof e&&(n=e,e="*");var o,p=0,M=e.toLowerCase().match(Y)||[];if(d(n))for(;o=M[p++];)"+"===o[0]?(o=o.slice(1)||"*",(t[o]=t[o]||[]).unshift(n)):(t[o]=t[o]||[]).push(n)}}function Ke(t,e,n,o){var p={},M=t===Ge;function b(c){var r;return p[c]=!0,g.each(t[c]||[],(function(t,c){var z=c(e,n,o);return"string"!=typeof z||M||p[z]?M?!(r=z):void 0:(e.dataTypes.unshift(z),b(z),!1)})),r}return b(e.dataTypes[0])||!p["*"]&&b("*")}function Qe(t,e){var n,o,p=g.ajaxSettings.flatOptions||{};for(n in e)void 0!==e[n]&&((p[n]?t:o||(o={}))[n]=e[n]);return o&&g.extend(!0,t,o),t}$e.href=Ne.href,g.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ne.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(Ne.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Ye,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":g.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(t,e){return e?Qe(Qe(t,g.ajaxSettings),e):Qe(g.ajaxSettings,t)},ajaxPrefilter:Ve(He),ajaxTransport:Ve(Ge),ajax:function(t,e){"object"==typeof t&&(e=t,t=void 0),e=e||{};var n,p,M,b,c,r,z,a,i,O,s=g.ajaxSetup({},e),A=s.context||s,u=s.context&&(A.nodeType||A.jquery)?g(A):g.event,l=g.Deferred(),d=g.Callbacks("once memory"),f=s.statusCode||{},h={},W={},v="canceled",R={readyState:0,getResponseHeader:function(t){var e;if(z){if(!b)for(b={};e=Ue.exec(M);)b[e[1].toLowerCase()+" "]=(b[e[1].toLowerCase()+" "]||[]).concat(e[2]);e=b[t.toLowerCase()+" "]}return null==e?null:e.join(", ")},getAllResponseHeaders:function(){return z?M:null},setRequestHeader:function(t,e){return null==z&&(t=W[t.toLowerCase()]=W[t.toLowerCase()]||t,h[t]=e),this},overrideMimeType:function(t){return null==z&&(s.mimeType=t),this},statusCode:function(t){var e;if(t)if(z)R.always(t[R.status]);else for(e in t)f[e]=[f[e],t[e]];return this},abort:function(t){var e=t||v;return n&&n.abort(e),m(0,e),this}};if(l.promise(R),s.url=((t||s.url||Ne.href)+"").replace(Fe,Ne.protocol+"//"),s.type=e.method||e.type||s.method||s.type,s.dataTypes=(s.dataType||"*").toLowerCase().match(Y)||[""],null==s.crossDomain){r=q.createElement("a");try{r.href=s.url,r.href=r.href,s.crossDomain=$e.protocol+"//"+$e.host!=r.protocol+"//"+r.host}catch(t){s.crossDomain=!0}}if(s.data&&s.processData&&"string"!=typeof s.data&&(s.data=g.param(s.data,s.traditional)),Ke(He,s,e,R),z)return R;for(i in(a=g.event&&s.global)&&0==g.active++&&g.event.trigger("ajaxStart"),s.type=s.type.toUpperCase(),s.hasContent=!je.test(s.type),p=s.url.replace(De,""),s.hasContent?s.data&&s.processData&&0===(s.contentType||"").indexOf("application/x-www-form-urlencoded")&&(s.data=s.data.replace(Ie,"+")):(O=s.url.slice(p.length),s.data&&(s.processData||"string"==typeof s.data)&&(p+=(Te.test(p)?"&":"?")+s.data,delete s.data),!1===s.cache&&(p=p.replace(Pe,"$1"),O=(Te.test(p)?"&":"?")+"_="+Ee.guid+++O),s.url=p+O),s.ifModified&&(g.lastModified[p]&&R.setRequestHeader("If-Modified-Since",g.lastModified[p]),g.etag[p]&&R.setRequestHeader("If-None-Match",g.etag[p])),(s.data&&s.hasContent&&!1!==s.contentType||e.contentType)&&R.setRequestHeader("Content-Type",s.contentType),R.setRequestHeader("Accept",s.dataTypes[0]&&s.accepts[s.dataTypes[0]]?s.accepts[s.dataTypes[0]]+("*"!==s.dataTypes[0]?", "+Ye+"; q=0.01":""):s.accepts["*"]),s.headers)R.setRequestHeader(i,s.headers[i]);if(s.beforeSend&&(!1===s.beforeSend.call(A,R,s)||z))return R.abort();if(v="abort",d.add(s.complete),R.done(s.success),R.fail(s.error),n=Ke(Ge,s,e,R)){if(R.readyState=1,a&&u.trigger("ajaxSend",[R,s]),z)return R;s.async&&s.timeout>0&&(c=o.setTimeout((function(){R.abort("timeout")}),s.timeout));try{z=!1,n.send(h,m)}catch(t){if(z)throw t;m(-1,t)}}else m(-1,"No Transport");function m(t,e,b,r){var i,O,q,h,W,v=e;z||(z=!0,c&&o.clearTimeout(c),n=void 0,M=r||"",R.readyState=t>0?4:0,i=t>=200&&t<300||304===t,b&&(h=function(t,e,n){for(var o,p,M,b,c=t.contents,r=t.dataTypes;"*"===r[0];)r.shift(),void 0===o&&(o=t.mimeType||e.getResponseHeader("Content-Type"));if(o)for(p in c)if(c[p]&&c[p].test(o)){r.unshift(p);break}if(r[0]in n)M=r[0];else{for(p in n){if(!r[0]||t.converters[p+" "+r[0]]){M=p;break}b||(b=p)}M=M||b}if(M)return M!==r[0]&&r.unshift(M),n[M]}(s,R,b)),!i&&g.inArray("script",s.dataTypes)>-1&&g.inArray("json",s.dataTypes)<0&&(s.converters["text script"]=function(){}),h=function(t,e,n,o){var p,M,b,c,r,z={},a=t.dataTypes.slice();if(a[1])for(b in t.converters)z[b.toLowerCase()]=t.converters[b];for(M=a.shift();M;)if(t.responseFields[M]&&(n[t.responseFields[M]]=e),!r&&o&&t.dataFilter&&(e=t.dataFilter(e,t.dataType)),r=M,M=a.shift())if("*"===M)M=r;else if("*"!==r&&r!==M){if(!(b=z[r+" "+M]||z["* "+M]))for(p in z)if((c=p.split(" "))[1]===M&&(b=z[r+" "+c[0]]||z["* "+c[0]])){!0===b?b=z[p]:!0!==z[p]&&(M=c[0],a.unshift(c[1]));break}if(!0!==b)if(b&&t.throws)e=b(e);else try{e=b(e)}catch(t){return{state:"parsererror",error:b?t:"No conversion from "+r+" to "+M}}}return{state:"success",data:e}}(s,h,R,i),i?(s.ifModified&&((W=R.getResponseHeader("Last-Modified"))&&(g.lastModified[p]=W),(W=R.getResponseHeader("etag"))&&(g.etag[p]=W)),204===t||"HEAD"===s.type?v="nocontent":304===t?v="notmodified":(v=h.state,O=h.data,i=!(q=h.error))):(q=v,!t&&v||(v="error",t<0&&(t=0))),R.status=t,R.statusText=(e||v)+"",i?l.resolveWith(A,[O,v,R]):l.rejectWith(A,[R,v,q]),R.statusCode(f),f=void 0,a&&u.trigger(i?"ajaxSuccess":"ajaxError",[R,s,i?O:q]),d.fireWith(A,[R,v]),a&&(u.trigger("ajaxComplete",[R,s]),--g.active||g.event.trigger("ajaxStop")))}return R},getJSON:function(t,e,n){return g.get(t,e,n,"json")},getScript:function(t,e){return g.get(t,void 0,e,"script")}}),g.each(["get","post"],(function(t,e){g[e]=function(t,n,o,p){return d(n)&&(p=p||o,o=n,n=void 0),g.ajax(g.extend({url:t,type:e,dataType:p,data:n,success:o},g.isPlainObject(t)&&t))}})),g.ajaxPrefilter((function(t){var e;for(e in t.headers)"content-type"===e.toLowerCase()&&(t.contentType=t.headers[e]||"")})),g._evalUrl=function(t,e,n){return g.ajax({url:t,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(t){g.globalEval(t,e,n)}})},g.fn.extend({wrapAll:function(t){var e;return this[0]&&(d(t)&&(t=t.call(this[0])),e=g(t,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&e.insertBefore(this[0]),e.map((function(){for(var t=this;t.firstElementChild;)t=t.firstElementChild;return t})).append(this)),this},wrapInner:function(t){return d(t)?this.each((function(e){g(this).wrapInner(t.call(this,e))})):this.each((function(){var e=g(this),n=e.contents();n.length?n.wrapAll(t):e.append(t)}))},wrap:function(t){var e=d(t);return this.each((function(n){g(this).wrapAll(e?t.call(this,n):t)}))},unwrap:function(t){return this.parent(t).not("body").each((function(){g(this).replaceWith(this.childNodes)})),this}}),g.expr.pseudos.hidden=function(t){return!g.expr.pseudos.visible(t)},g.expr.pseudos.visible=function(t){return!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)},g.ajaxSettings.xhr=function(){try{return new o.XMLHttpRequest}catch(t){}};var Je={0:200,1223:204},Ze=g.ajaxSettings.xhr();l.cors=!!Ze&&"withCredentials"in Ze,l.ajax=Ze=!!Ze,g.ajaxTransport((function(t){var e,n;if(l.cors||Ze&&!t.crossDomain)return{send:function(p,M){var b,c=t.xhr();if(c.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(b in t.xhrFields)c[b]=t.xhrFields[b];for(b in t.mimeType&&c.overrideMimeType&&c.overrideMimeType(t.mimeType),t.crossDomain||p["X-Requested-With"]||(p["X-Requested-With"]="XMLHttpRequest"),p)c.setRequestHeader(b,p[b]);e=function(t){return function(){e&&(e=n=c.onload=c.onerror=c.onabort=c.ontimeout=c.onreadystatechange=null,"abort"===t?c.abort():"error"===t?"number"!=typeof c.status?M(0,"error"):M(c.status,c.statusText):M(Je[c.status]||c.status,c.statusText,"text"!==(c.responseType||"text")||"string"!=typeof c.responseText?{binary:c.response}:{text:c.responseText},c.getAllResponseHeaders()))}},c.onload=e(),n=c.onerror=c.ontimeout=e("error"),void 0!==c.onabort?c.onabort=n:c.onreadystatechange=function(){4===c.readyState&&o.setTimeout((function(){e&&n()}))},e=e("abort");try{c.send(t.hasContent&&t.data||null)}catch(t){if(e)throw t}},abort:function(){e&&e()}}})),g.ajaxPrefilter((function(t){t.crossDomain&&(t.contents.script=!1)})),g.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(t){return g.globalEval(t),t}}}),g.ajaxPrefilter("script",(function(t){void 0===t.cache&&(t.cache=!1),t.crossDomain&&(t.type="GET")})),g.ajaxTransport("script",(function(t){var e,n;if(t.crossDomain||t.scriptAttrs)return{send:function(o,p){e=g(" - - diff --git a/resources/views/components/popup.blade.php b/resources/views/components/popup.blade.php index 8061c29bd..6ae63a644 100644 --- a/resources/views/components/popup.blade.php +++ b/resources/views/components/popup.blade.php @@ -6,10 +6,10 @@ x-transition:enter-start="translate-y-full" x-transition:enter-end="translate-y-0" x-transition:leave="transition ease-in duration-300" x-transition:leave-start="translate-y-0" x-transition:leave-end="translate-y-full" x-init="setTimeout(() => { bannerVisible = true }, bannerVisibleAfter);" - class="fixed bottom-0 right-0 w-full h-auto duration-300 ease-out sm:px-5 sm:pb-5 sm:w-[26rem] lg:w-full z-[999]" + class="fixed bottom-0 right-0 w-full h-auto duration-300 ease-out sm:px-5 sm:pb-5 w-full z-[999]" x-cloak>
+ class="flex items-center flex-col justify-between w-full h-full max-w-4xl p-6 mx-auto bg-white border shadow-lg lg:border-t dark:border-coolgray-300 dark:bg-coolgray-100 lg:p-8 lg:flex-row sm:rounded">
@if (isset($icon)) @@ -23,14 +23,12 @@

{{ $description }}

-
-
diff --git a/resources/views/components/pricing-plans.blade.php b/resources/views/components/pricing-plans.blade.php index 5977323f2..873ecfc47 100644 --- a/resources/views/components/pricing-plans.blade.php +++ b/resources/views/components/pricing-plans.blade.php @@ -34,15 +34,16 @@
- {{--
+

Unlimited Trial Get Started

-

Start self-hosting without limits with +

Start self-hosting without limits + with our OSS version. Same features as the paid version, but you have to manage by yourself.

-
--}} +

Ultimate

-

+

Custom {{-- pay-as-you-go --}} @@ -198,7 +200,7 @@ {{-- /month + VAT --}}

- + pay-as-you-go @@ -213,8 +215,8 @@ single location.

  • - diff --git a/resources/views/components/resources/breadcrumbs.blade.php b/resources/views/components/resources/breadcrumbs.blade.php index ea12495a1..f9733f63a 100644 --- a/resources/views/components/resources/breadcrumbs.blade.php +++ b/resources/views/components/resources/breadcrumbs.blade.php @@ -44,7 +44,7 @@ @if ($resource->getMorphClass() == 'App\Models\Service') @else - + @endif diff --git a/resources/views/components/server/navbar.blade.php b/resources/views/components/server/navbar.blade.php index c24787e97..518831c83 100644 --- a/resources/views/components/server/navbar.blade.php +++ b/resources/views/components/server/navbar.blade.php @@ -1,50 +1,50 @@
    - + + + + + + + Close + + +

    Server

    - + @if ($server->proxySet()) + + @endif
    -
    {{ data_get($server, 'name') }}.
    +
    {{ data_get($server, 'name') }}
@if ($environment->isEmpty()) - + Add New Resource @else
- +
+ +