diff --git a/.env.example b/.env.example index 251eb4d9..a5b5d365 100644 --- a/.env.example +++ b/.env.example @@ -75,8 +75,7 @@ GOTENBERG_URL=http://gotenberg:3000 # Local setup NGINX_HOST_NAME=solidtime.test NETWORK_NAME=reverse-proxy-docker-traefik_routing -FORWARD_DB_PORT=5432 -FORWARD_WEB_PORT=8083 +FORWARD_DB_PORT=54329 VITE_HOST_NAME=vite.solidtime.test VITE_APP_NAME="${APP_NAME}" #SAIL_XDEBUG_MODE=develop,debug,coverage diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 4f2c9b4f..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -github: solidtime-io diff --git a/.github/ISSUE_TEMPLATE/1_bug_report.yml b/.github/ISSUE_TEMPLATE/1_bug_report.yml deleted file mode 100644 index 5c610ee8..00000000 --- a/.github/ISSUE_TEMPLATE/1_bug_report.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Bug Report -description: "Report a bug" -body: - - type: markdown - attributes: - value: | - Before creating a new bug report, please check that there isn't already a similar issue. - - - type: textarea - attributes: - label: Description - description: A clear and concise description of what the bug is. - validations: - required: true - - - type: textarea - attributes: - label: "Steps To Reproduce" - description: How do you trigger this bug? Please walk us through it step by step. - value: | - 1. - 2. - 3. - ... - validations: - required: false - - - type: dropdown - attributes: - label: "Self-hosted or Cloud?" - options: - - Self-Hosted - - solidtime Cloud - - Both - - - type: input - attributes: - label: "Version of solidtime: (for self-hosted)" - validations: - required: false - - - type: input - attributes: - label: "solidtime self-hosting guide: (for self-hosted)" - description: "Did you use the official guide to self-host solidtime? If yes, which one?" - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index b04f17c2..00000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,8 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: 🚀 Feature Request - url: https://github.com/solidtime-io/solidtime/discussions/new?category=feature-requests - about: Share ideas for new features - - name: ❓ Ask a Question - url: https://github.com/solidtime-io/solidtime/discussions/new?category=general - about: Ask the community for help diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 46d9356b..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,48 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" - target-branch: "main" - - package-ecosystem: "docker" - directory: "/" - schedule: - interval: "daily" - target-branch: "main" - - package-ecosystem: "composer" - directory: "/" - schedule: - interval: "weekly" - target-branch: "main" - groups: - major-updates: - update-types: - - "major" - minor-updates: - update-types: - - "minor" - - "patch" - security-updates: - applies-to: version-updates - update-types: - - "minor" - - "patch" - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "weekly" - target-branch: "main" - groups: - major-updates: - update-types: - - "major" - minor-updates: - update-types: - - "minor" - - "patch" - security-updates: - applies-to: version-updates - update-types: - - "minor" - - "patch" diff --git a/.github/workflows/build-private.yml b/.github/workflows/build-private.yml deleted file mode 100644 index c335519b..00000000 --- a/.github/workflows/build-private.yml +++ /dev/null @@ -1,199 +0,0 @@ -on: - push: - branches: - - main - - develop - tags: - - '*' - pull_request: - paths: - - '.github/workflows/build-private.yml' - - 'docker/prod/**' - workflow_dispatch: -permissions: - contents: read - -name: Build - Private -jobs: - build: - runs-on: ubuntu-latest - timeout-minutes: 20 - - - steps: - - name: "Check out code" - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag - - - name: "Get build" - id: build - run: echo "build=$(git rev-parse --short=8 HEAD)" >> "$GITHUB_OUTPUT" - - - name: "Get Previous tag (normal push)" - id: previoustag - if: ${{ !startsWith(github.ref, 'refs/tags/v') }} - uses: "WyriHaximus/github-action-get-previous-tag@v1" - with: - prefix: "v" - - - name: "Get version" - id: version - run: | - if ${{ !startsWith(github.ref, 'refs/tags/v') }}; then - if ${{ startsWith(steps.previoustag.outputs.tag, 'v') }}; then - version=$(echo "${{ steps.previoustag.outputs.tag }}" | cut -c 2-) - echo "app_version=${version}" >> "$GITHUB_OUTPUT" - else - echo "ERROR: No previous tag found"; - exit 1; - fi - else - version=$(echo "${{ github.ref }}" | cut -c 12-) - echo "app_version=${version}" >> "$GITHUB_OUTPUT" - fi - - - name: "Copy .env template for production" - run: | - cp .env.production .env - rm .env.production .env.ci .env.example - - - name: "Add version to .env" - run: sed -i 's/APP_VERSION=0.0.0/APP_VERSION=${{ steps.version.outputs.app_version }}/g' .env - - - name: "Add build to .env" - run: sed -i 's/APP_BUILD=0/APP_BUILD=${{ steps.build.outputs.build }}/g' .env - - - name: "Output .env" - run: cat .env - - - name: "Use Node.js" - uses: actions/setup-node@v4 - with: - node-version: '20.x' - - - name: "Checkout billing extension" - uses: actions/checkout@v4 - with: - repository: solidtime-io/extension-billing - path: extensions/Billing - ssh-key: ${{ secrets.SSH_PRIVATE_KEY_BILLING_EXTENSION }} - - - name: "Install dependencies in billing extension" - uses: php-actions/composer@v6 - env: - COMPOSER_AUTH: '{"http-basic": {"spark.laravel.com": {"username": "gregor@vostrak.at", "password": "${{ secrets.LARAVEL_SPARK_API_KEY }}"}}}' - with: - working_dir: "extensions/Billing" - command: install - only_args: --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative - php_version: 8.3 - - - name: "Install npm dependencies in billing extension" - run: cd extensions/Billing && npm ci - - - name: "Checkout services extension" - uses: actions/checkout@v4 - with: - repository: solidtime-io/extension-services - path: extensions/Services - ssh-key: ${{ secrets.SSH_PRIVATE_KEY_SERVICES_EXTENSION }} - - - name: "Install composer dependencies in services extension" - uses: php-actions/composer@v6 - with: - working_dir: "extensions/Services" - command: install - only_args: --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative - php_version: 8.3 - - - name: "Install npm dependencies in services extension" - run: cd extensions/Services && npm ci - - - name: "Checkout invoicing extension" - uses: actions/checkout@v4 - with: - repository: solidtime-io/extension-invoicing - path: extensions/Invoicing - ssh-key: ${{ secrets.SSH_PRIVATE_KEY_INVOICING_EXTENSION }} - - - name: "Install composer dependencies in invoicing extension" - uses: php-actions/composer@v6 - with: - working_dir: "extensions/Invoicing" - command: install - only_args: --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative - php_version: 8.3 - - - name: "Install npm dependencies in invoicing extension" - run: cd extensions/Invoicing && npm ci - - - name: "Setup PHP with PECL extension" - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - extensions: mbstring, dom, fileinfo, pgsql - - - name: "Install dependencies" - uses: php-actions/composer@v6 - if: steps.cache-vendor.outputs.cache-hit != 'true' # Skip if cache hit - with: - command: install - only_args: --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative - php_version: 8.3 - - - name: "Activate billing extension" - run: php artisan module:enable Billing - - - name: "Activate services extension" - run: php artisan module:enable Services - - - name: "Activate invoicing extension" - run: php artisan module:enable Invoicing - - - name: "Install npm dependencies" - run: npm ci - - - name: "Build" - run: npm run build - env: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - - - name: "Login to GitHub Container Registry" - uses: docker/login-action@v3 - with: - registry: rg.fr-par.scw.cloud/solidtime - username: nologin - password: ${{ secrets.SCALEWAY_REGISTRY_TOKEN }} - - - name: "Docker meta" - id: "meta" - uses: docker/metadata-action@v5 - with: - images: rg.fr-par.scw.cloud/solidtime/solidtime - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=sha,format=long - - - name: "Set up QEMU" - uses: docker/setup-qemu-action@v3 - - - name: "Set up Docker Buildx" - uses: docker/setup-buildx-action@v3 - - - name: "Build and push" - uses: docker/build-push-action@v6 - with: - context: . - build-args: | - DOCKER_FILES_BASE_PATH=docker/prod/ - file: docker/prod/Dockerfile - push: true - platforms: linux/amd64 - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/build-public.yml b/.github/workflows/build-public.yml deleted file mode 100644 index d91d6e51..00000000 --- a/.github/workflows/build-public.yml +++ /dev/null @@ -1,218 +0,0 @@ -on: - push: - branches: - - main - - develop - tags: - - '*' - pull_request: - paths: - - '.github/workflows/build-public.yml' - - 'docker/prod/**' - workflow_dispatch: - -permissions: - packages: write - contents: read - attestations: write - id-token: write - -env: - DOCKERHUB_REPO: solidtime/solidtime - GHCR_REPO: ghcr.io/solidtime-io/solidtime - -name: Build - Public -jobs: - build: - strategy: - matrix: - include: - - runs-on: "ubuntu-24.04-arm" - platform: "linux/arm64" - - runs-on: "ubuntu-24.04" - platform: "linux/amd64" - runs-on: ${{ matrix.runs-on }} - timeout-minutes: 90 - - steps: - - name: "Check out code" - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag - - - name: "Get build" - id: release-build - run: echo "build=$(git rev-parse --short=8 HEAD)" >> "$GITHUB_OUTPUT" - - - name: "Get Previous tag (normal push)" - id: previoustag - if: ${{ !startsWith(github.ref, 'refs/tags/v') }} - uses: "WyriHaximus/github-action-get-previous-tag@v1" - with: - prefix: "v" - - - name: "Get version" - id: release-version - run: | - if ${{ !startsWith(github.ref, 'refs/tags/v') }}; then - if ${{ startsWith(steps.previoustag.outputs.tag, 'v') }}; then - version=$(echo "${{ steps.previoustag.outputs.tag }}" | cut -c 2-) - echo "app_version=${version}" >> "$GITHUB_OUTPUT" - else - echo "ERROR: No previous tag found"; - exit 1; - fi - else - version=$(echo "${{ github.ref }}" | cut -c 12-) - echo "app_version=${version}" >> "$GITHUB_OUTPUT" - fi - - - name: "Copy .env template for production" - run: | - cp .env.production .env - rm .env.production .env.ci .env.example - - - name: "Add version to .env" - run: sed -i 's/APP_VERSION=0.0.0/APP_VERSION=${{ steps.release-version.outputs.app_version }}/g' .env - - - name: "Add build to .env" - run: sed -i 's/APP_BUILD=0/APP_BUILD=${{ steps.release-build.outputs.build }}/g' .env - - - name: "Output .env" - run: cat .env - - - name: "Setup PHP with PECL extension" - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - extensions: mbstring, dom, fileinfo, pgsql - - - name: "Install dependencies" - run: composer install --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative - if: steps.cache-vendor.outputs.cache-hit != 'true' # Skip if cache hit - - - name: "Use Node.js" - uses: actions/setup-node@v4 - with: - node-version: '20.x' - - - name: "Install npm dependencies" - run: npm ci - - - name: "Build" - run: npm run build - - - name: "Prepare" - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - - - name: "Docker meta" - id: "meta" - uses: docker/metadata-action@v5 - with: - images: | - ${{ env.DOCKERHUB_REPO }} - ${{ env.GHCR_REPO }} - - - name: "Login to Docker Hub Container Registry" - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: "Login to GitHub Container Registry" - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: "Set up QEMU" - uses: docker/setup-qemu-action@v3 - - - name: "Set up Docker Buildx" - uses: docker/setup-buildx-action@v3 - - - name: "Build and push by digest" - id: build - uses: docker/build-push-action@v6 - with: - context: . - file: docker/prod/Dockerfile - build-args: | - DOCKER_FILES_BASE_PATH=docker/prod/ - platforms: ${{ matrix.platform }} - labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,"name=${{ env.DOCKERHUB_REPO }},${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=true - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: "Export digest" - run: | - mkdir -p ${{ runner.temp }}/digests - digest="${{ steps.build.outputs.digest }}" - touch "${{ runner.temp }}/digests/${digest#sha256:}" - - - name: "Upload digest" - uses: actions/upload-artifact@v4 - with: - name: digests-${{ env.PLATFORM_PAIR }} - path: ${{ runner.temp }}/digests/* - if-no-files-found: error - retention-days: 1 - - merge: - runs-on: ubuntu-latest - timeout-minutes: 90 - needs: - - build - steps: - - name: "Download digests" - uses: actions/download-artifact@v4 - with: - path: ${{ runner.temp }}/digests - pattern: digests-* - merge-multiple: true - - - name: "Login to Docker Hub" - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: "Login to GHCR" - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: "Set up Docker Buildx" - uses: docker/setup-buildx-action@v3 - - - name: "Docker meta" - id: meta - uses: docker/metadata-action@v5 - with: - images: | - ${{ env.DOCKERHUB_REPO }} - ${{ env.GHCR_REPO }} - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - - - name: "Create manifest list and push" - working-directory: ${{ runner.temp }}/digests - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.DOCKERHUB_REPO }}@sha256:%s ' *) - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.GHCR_REPO }}@sha256:%s ' *) - - - name: "Inspect image" - run: | - docker buildx imagetools inspect ${{ env.DOCKERHUB_REPO }}:${{ steps.meta.outputs.version }} - docker buildx imagetools inspect ${{ env.GHCR_REPO }}:${{ steps.meta.outputs.version }} diff --git a/.github/workflows/generate-api-docs.yml b/.github/workflows/generate-api-docs.yml deleted file mode 100644 index bdb92036..00000000 --- a/.github/workflows/generate-api-docs.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Generate API docs -on: - push: - branches: - - main -permissions: - contents: read - -jobs: - api_docs: - runs-on: ubuntu-latest - timeout-minutes: 10 - - services: - pgsql_test: - image: postgres:15 - env: - PGPASSWORD: 'root' - POSTGRES_DB: 'laravel' - POSTGRES_USER: 'root' - POSTGRES_PASSWORD: 'root' - ports: - - 5432:5432 - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - steps: - - name: "Checkout code" - uses: actions/checkout@v4 - - - name: "Setup PHP" - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv - - - name: "Run composer install" - run: composer install -n --prefer-dist - - - name: "Create build directory" - run: mkdir build - - - name: Prepare Laravel Application - run: | - cp .env.ci .env - php artisan migrate - - - name: "Export API docs" - run: php artisan scramble:export --path=build/api-docs.json - - - name: "Upload API docs to GitHub" - uses: actions/upload-artifact@v4 - with: - name: api-docs.json - path: build/api-docs.json - - - name: "Download Fastfront CLI" - run: curl https://fastfront-cli.s3.fr-par.scw.cloud/fastfront-cli.phar -o fastfront-cli.phar - - - name: "Deploy with Fastfront" - run: php fastfront-cli.phar deploy 9beab6cf-f459-446b-85f1-38ec007cf457 ./build - env: - FASTFRONT_API_KEY: ${{ secrets.FASTFRONT_API_DOCS_API_KEY }} diff --git a/.github/workflows/npm-build.yml b/.github/workflows/npm-build.yml index 6201b91a..55293f9d 100644 --- a/.github/workflows/npm-build.yml +++ b/.github/workflows/npm-build.yml @@ -16,7 +16,7 @@ jobs: - name: "Setup PHP (for Ziggy)" uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.4' extensions: intl, zip coverage: none diff --git a/.github/workflows/npm-format-check.yml b/.github/workflows/npm-format-check.yml new file mode 100644 index 00000000..088e5211 --- /dev/null +++ b/.github/workflows/npm-format-check.yml @@ -0,0 +1,23 @@ +name: NPM Format Check + +on: [push] + +jobs: + format-check: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: "Checkout code" + uses: actions/checkout@v4 + + - name: "Use Node.js" + uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: "Install npm dependencies" + run: npm ci + + - name: "Check code formatting" + run: npm run format:check \ No newline at end of file diff --git a/.github/workflows/npm-publish-api.yml b/.github/workflows/npm-publish-api.yml deleted file mode 100644 index a1b9501b..00000000 --- a/.github/workflows/npm-publish-api.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Publish API package to NPM -on: - workflow_dispatch -permissions: - contents: read -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - steps: - - name: "Checkout code" - uses: actions/checkout@v4 - # Setup .npmrc file to publish to npm - - name: Install root project dependencies - run: npm ci - - uses: actions/setup-node@v4 - with: - node-version: '20.x' - registry-url: 'https://registry.npmjs.org' - - name: Install dependencies - run: npm ci - working-directory: ./resources/js/packages/api - - name: Build package - run: npm run build - working-directory: ./resources/js/packages/api - - name: Publish Package - run: npm publish --provenance --access public - working-directory: ./resources/js/packages/api - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/npm-publish-ui.yml b/.github/workflows/npm-publish-ui.yml deleted file mode 100644 index 75e851c3..00000000 --- a/.github/workflows/npm-publish-ui.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Publish UI package to NPM -on: - workflow_dispatch -permissions: - contents: read -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - steps: - - name: "Checkout code" - uses: actions/checkout@v4 - # Setup .npmrc file to publish to npm - - uses: actions/setup-node@v4 - with: - node-version: '20.x' - registry-url: 'https://registry.npmjs.org' - - name: Install root project dependencies - run: npm ci - - name: Install package dependencies - run: npm ci - working-directory: ./resources/js/packages/ui - - name: Build package - run: npm run build - working-directory: ./resources/js/packages/ui - - name: Publish Package - run: npm publish --provenance --access public - working-directory: ./resources/js/packages/ui - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/npm-typecheck.yml b/.github/workflows/npm-typecheck.yml index e98a6c7c..ead03a74 100644 --- a/.github/workflows/npm-typecheck.yml +++ b/.github/workflows/npm-typecheck.yml @@ -15,7 +15,7 @@ jobs: - name: "Setup PHP (for Ziggy)" uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.4' extensions: intl, zip coverage: none diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 77f8d5cd..dee510a7 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -14,7 +14,7 @@ jobs: - name: "Setup PHP" uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.4' extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv coverage: none diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 57144bd7..97a0eb95 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -8,7 +8,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - postgres_version: [ 15, 16, 17 ] + postgres_version: [ 17 ] services: pgsql_test: @@ -41,7 +41,7 @@ jobs: - name: "Setup PHP" uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: '8.4' extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv coverage: pcov diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index d0f9b805..3c7e7691 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -11,7 +11,7 @@ jobs: mailpit: image: 'axllent/mailpit:latest' pgsql_test: - image: postgres:15 + image: postgres:17 env: PGPASSWORD: 'root' POSTGRES_DB: 'laravel' @@ -37,8 +37,8 @@ jobs: - name: "Setup PHP" uses: shivammathur/setup-php@v2 with: - php-version: '8.3' - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv + php-version: '8.4' + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_sqlite, pdo_pgsql, bcmath, soap, intl, gd, exif, iconv coverage: none - name: "Run composer install" @@ -47,6 +47,10 @@ jobs: - name: "Prepare Laravel Application" run: | cp .env.ci .env + echo "DB_HOST=127.0.0.1" >> .env + echo "DB_READ_HOST=127.0.0.1" >> .env + echo "DB_WRITE_HOST=127.0.0.1" >> .env + sleep 10 php artisan key:generate php artisan passport:keys php artisan migrate --seed diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..6157e53f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,27 @@ +# Ignore build outputs +node_modules/ +vendor/ +storage/ +bootstrap/cache/ +public/build/ +public/hot/ + +# Ignore lock files +package-lock.json +composer.lock + +# Ignore generated files +*.min.js +*.min.css + +# Ignore test results +test-results/ +playwright-report/ + +# Ignore IDE files +.idea/ +.vscode/ + +# Ignore OS files +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json index 62762f5c..d689166c 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -3,5 +3,6 @@ "tabWidth": 4, "singleQuote": true, "bracketSameLine": true, - "quoteProps": "preserve" + "quoteProps": "preserve", + "printWidth": 100 } diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index e0e0ce3c..00000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,42 +0,0 @@ -# Code of Conduct - -The goal is to create a community that is open and welcoming to all individuals. -To achieve this, we have developed a code of conduct that outlines the expectations for behavior of all members of our community. - -## Pledge - -This community is founded on respect and understanding. -All members are expected to treat others with respect and empathy, and to not tolerate any form of discrimination, -harassment, or attacks. - -## Expectations - -Examples of behavior that contributes to creating a positive environment include: - -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -- The use of sexualized language or imagery and sexual attention or advances -- Trolling, insulting/derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or electronic address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a professional setting - -## Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate -and fair corrective action in response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, -issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily -or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. - -## Contact - -If you feel uncomfortable or believe that someone has violated the code of conduct, please contact us at [hello@solidtime.io](mailto:hello@solidtime.io). -We will thoroughly investigate the incident and aim for the best possible outcome. diff --git a/README.md b/README.md index c2ddc8a9..7943285b 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,9 @@ If you have a **feature request**, please [**create a discussion**](https://gith ## Contributing -This project is in a very early stage. The structure and APIs are still subject to change and not stable. -Therefore, we do not currently accept any contributions, unless you are a member of the team. +Please open an issue or start a discussion and wait for approval before submitting a pull request. This does not apply to tiny fixes or changes however, please keep in mind that we might not merge PRs for various reasons. -As soon as we feel comfortable enough that the application structure is stable enough, we will open up the project for contributions. +Please read the [CONTRIBUTING.md](./CONTRIBUTING.md) before sumbitting a Pull Request. We do accept contributions in the [documentation repository](https://github.com/solidtime-io/docs) f.e. to add new self-hosting guides. diff --git a/app/Http/Controllers/Api/V1/ChartController.php b/app/Http/Controllers/Api/V1/ChartController.php index 4ceb6acc..3e034df9 100644 --- a/app/Http/Controllers/Api/V1/ChartController.php +++ b/app/Http/Controllers/Api/V1/ChartController.php @@ -14,6 +14,8 @@ class ChartController extends Controller { /** + * Get chart data for the weekly project overview. + * * @throws AuthorizationException * * @operationId weeklyProjectOverview @@ -31,6 +33,8 @@ public function weeklyProjectOverview(Organization $organization, DashboardServi } /** + * Get chart data for the latest tasks. + * * @throws AuthorizationException * * @operationId latestTasks @@ -48,6 +52,8 @@ public function latestTasks(Organization $organization, DashboardService $dashbo } /** + * Get chart data for the last seven days. + * * @throws AuthorizationException * * @operationId lastSevenDays @@ -65,6 +71,8 @@ public function lastSevenDays(Organization $organization, DashboardService $dash } /** + * Get chart data for the latest team activity. + * * @throws AuthorizationException * * @operationId latestTeamActivity @@ -81,6 +89,8 @@ public function latestTeamActivity(Organization $organization, DashboardService } /** + * Get chart data for daily tracked hours. + * * @throws AuthorizationException * * @operationId dailyTrackedHours @@ -98,6 +108,8 @@ public function dailyTrackedHours(Organization $organization, DashboardService $ } /** + * Get chart data for total weekly time. + * * @throws AuthorizationException * * @operationId totalWeeklyTime @@ -115,6 +127,8 @@ public function totalWeeklyTime(Organization $organization, DashboardService $da } /** + * Get chart data for total weekly billable time. + * * @throws AuthorizationException * * @operationId totalWeeklyBillableTime @@ -132,6 +146,8 @@ public function totalWeeklyBillableTime(Organization $organization, DashboardSer } /** + * Get chart data for total weekly billable amount. + * * @throws AuthorizationException * * @operationId totalWeeklyBillableAmount @@ -154,6 +170,8 @@ public function totalWeeklyBillableAmount(Organization $organization, DashboardS } /** + * Get chart data for weekly history. + * * @throws AuthorizationException * * @operationId weeklyHistory diff --git a/app/Http/Controllers/Api/V1/ClientController.php b/app/Http/Controllers/Api/V1/ClientController.php index 70cf0900..38e04b60 100644 --- a/app/Http/Controllers/Api/V1/ClientController.php +++ b/app/Http/Controllers/Api/V1/ClientController.php @@ -105,13 +105,14 @@ public function destroy(Organization $organization, Client $client): JsonRespons { return response()->json(null, 204); - $this->checkPermission($organization, 'clients:delete', $client); + // $this->checkPermission($organization, 'clients:delete', $client); - if ($client->projects()->exists()) { - throw new EntityStillInUseApiException('client', 'project'); - } + // if ($client->projects()->exists()) { + // throw new EntityStillInUseApiException('client', 'project'); + // } - $client->delete(); + // $client->delete(); + // return response()->json(null, 204); } } diff --git a/app/Http/Controllers/Api/V1/OrganizationController.php b/app/Http/Controllers/Api/V1/OrganizationController.php index fa9c00bb..e3a0b1eb 100644 --- a/app/Http/Controllers/Api/V1/OrganizationController.php +++ b/app/Http/Controllers/Api/V1/OrganizationController.php @@ -7,14 +7,9 @@ use App\Enums\Role; use App\Http\Requests\V1\Organization\OrganizationUpdateRequest; use App\Http\Resources\V1\Organization\OrganizationResource; -use App\Models\Client; -use App\Models\Member; use App\Models\Organization; -use App\Models\Project; -use App\Models\Tag; use App\Service\BillableRateService; use Illuminate\Auth\Access\AuthorizationException; -use Illuminate\Http\JsonResponse; class OrganizationController extends Controller { @@ -34,27 +29,6 @@ public function show(Organization $organization): OrganizationResource return new OrganizationResource($organization, $showBillableRate); } - /** - * Get counts for sidebar items - * - * @operationId getOrganizationCounts - * - * @throws AuthorizationException - */ - public function getCounts(Organization $organization): JsonResponse - { - $this->checkPermission($organization, 'organizations:view'); - - $counts = [ - 'projects' => Project::where('organization_id', $organization->id)->whereNull('archived_at')->count(), - 'clients' => Client::where('organization_id', $organization->id)->whereNull('archived_at')->count(), - 'members' => Member::where('organization_id', $organization->id)->count(), - 'tags' => Tag::where('organization_id', $organization->id)->count(), - ]; - - return response()->json(['data' => $counts]); - } - /** * Update organization * diff --git a/app/Http/Controllers/Api/V1/ProjectController.php b/app/Http/Controllers/Api/V1/ProjectController.php index d3b917ec..2acedfba 100644 --- a/app/Http/Controllers/Api/V1/ProjectController.php +++ b/app/Http/Controllers/Api/V1/ProjectController.php @@ -27,6 +27,7 @@ class ProjectController extends Controller protected function checkPermission(Organization $organization, string $permission, ?Project $project = null): void { parent::checkPermission($organization, $permission); + if ($project !== null && $project->organization_id !== $organization->id) { throw new AuthorizationException('Project does not belong to organization'); } @@ -167,21 +168,22 @@ public function destroy(Organization $organization, Project $project): JsonRespo { return response()->json(null, 204); - $this->checkPermission($organization, 'projects:delete', $project); + // $this->checkPermission($organization, 'projects:delete', $project); - if ($project->tasks()->exists()) { - throw new EntityStillInUseApiException('project', 'task'); - } - if ($project->timeEntries()->exists()) { - throw new EntityStillInUseApiException('project', 'time_entry'); - } + // if ($project->tasks()->exists()) { + // throw new EntityStillInUseApiException('project', 'task'); + // } + // if ($project->timeEntries()->exists()) { + // throw new EntityStillInUseApiException('project', 'time_entry'); + // } - DB::transaction(function () use (&$project): void { - $project->members->each(function (ProjectMember $member): void { - $member->delete(); - }); - $project->delete(); - }); + // DB::transaction(function () use (&$project): void { + // $project->members->each(function (ProjectMember $member): void { + // $member->delete(); + // }); + // $project->delete(); + // }); + // return response()->json(null, 204); } } diff --git a/app/Http/Controllers/Api/V1/TimeEntryController.php b/app/Http/Controllers/Api/V1/TimeEntryController.php index a2bb23ac..a36c9f71 100644 --- a/app/Http/Controllers/Api/V1/TimeEntryController.php +++ b/app/Http/Controllers/Api/V1/TimeEntryController.php @@ -86,7 +86,8 @@ public function index(Organization $organization, TimeEntryIndexRequest $request $this->checkPermission($organization, 'time-entries:view:all'); } - $timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member); + $canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization); + $timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member, $canAccessPremiumFeatures); $totalCount = $timeEntriesQuery->count(); @@ -140,13 +141,15 @@ public function index(Organization $organization, TimeEntryIndexRequest $request /** * @return Builder */ - private function getTimeEntriesQuery(Organization $organization, TimeEntryIndexRequest|TimeEntryIndexExportRequest $request, ?Member $member): Builder + private function getTimeEntriesQuery(Organization $organization, TimeEntryIndexRequest|TimeEntryIndexExportRequest $request, ?Member $member, bool $canAccessPremiumFeatures): Builder { $select = TimeEntry::SELECT_COLUMNS; - if ($request->getRoundingType() !== null && $request->getRoundingMinutes() !== null) { + $roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null; + $roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null; + if ($roundingType !== null && $roundingMinutes !== null) { $select = array_diff($select, ['start', 'end']); - $select[] = DB::raw(app(TimeEntryService::class)->getStartSelectRawForRounding($request->getRoundingType(), $request->getRoundingMinutes()).' as start'); - $select[] = DB::raw(app(TimeEntryService::class)->getEndSelectRawForRounding($request->getRoundingType(), $request->getRoundingMinutes()).' as end'); + $select[] = DB::raw(app(TimeEntryService::class)->getStartSelectRawForRounding($roundingType, $roundingMinutes).' as start'); + $select[] = DB::raw(app(TimeEntryService::class)->getEndSelectRawForRounding($roundingType, $roundingMinutes).' as end'); } $timeEntriesQuery = TimeEntry::query() ->whereBelongsTo($organization, 'organization') @@ -184,18 +187,19 @@ public function indexExport(Organization $organization, TimeEntryIndexExportRequ } else { $this->checkPermission($organization, 'time-entries:view:all'); } + $canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization); $debug = $request->getDebug(); $format = $request->getFormatValue(); - if ($format === ExportFormat::PDF && ! $this->canAccessPremiumFeatures($organization)) { + if ($format === ExportFormat::PDF && ! $canAccessPremiumFeatures) { throw new FeatureIsNotAvailableInFreePlanApiException; } $user = $this->user(); $timezone = $user->timezone; $showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates; - $roundingType = $request->getRoundingType(); - $roundingMinutes = $request->getRoundingMinutes(); + $roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null; + $roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null; - $timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member); + $timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member, $canAccessPremiumFeatures); $timeEntriesQuery->with([ 'task', 'client', @@ -332,14 +336,15 @@ public function aggregate(Organization $organization, TimeEntryAggregateRequest } else { $this->checkPermission($organization, 'time-entries:view:all'); } + $canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization); $user = $this->user(); $showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates; $group1Type = $request->getGroup(); $group2Type = $request->getSubGroup(); $timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member); - $roundingType = $request->getRoundingType(); - $roundingMinutes = $request->getRoundingMinutes(); + $roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null; + $roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null; $aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntries( $timeEntriesAggregateQuery, @@ -380,6 +385,7 @@ public function aggregateExport(Organization $organization, TimeEntryAggregateEx } else { $this->checkPermission($organization, 'time-entries:view:all'); } + $canAccessPremiumFeatures = $this->canAccessPremiumFeatures($organization); $format = $request->getFormatValue(); if ($format === ExportFormat::PDF && ! $this->canAccessPremiumFeatures($organization)) { throw new FeatureIsNotAvailableInFreePlanApiException; @@ -391,8 +397,8 @@ public function aggregateExport(Organization $organization, TimeEntryAggregateEx $group = $request->getGroup(); $subGroup = $request->getSubGroup(); $timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member); - $roundingType = $request->getRoundingType(); - $roundingMinutes = $request->getRoundingMinutes(); + $roundingType = $canAccessPremiumFeatures ? $request->getRoundingType() : null; + $roundingMinutes = $canAccessPremiumFeatures ? $request->getRoundingMinutes() : null; $aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntriesWithDescriptions( $timeEntriesAggregateQuery->clone(), diff --git a/app/Http/Controllers/Api/V1/UserTimeEntryController.php b/app/Http/Controllers/Api/V1/UserTimeEntryController.php index 45ffeb4a..6cf53518 100644 --- a/app/Http/Controllers/Api/V1/UserTimeEntryController.php +++ b/app/Http/Controllers/Api/V1/UserTimeEntryController.php @@ -67,7 +67,7 @@ public function myActive(): JsonResponse return response()->json([ 'data' => null, 'message' => 'No active time entry', - ], 200); + ], 404); } } } diff --git a/composer.json b/composer.json index a9493aaf..df3d5ddc 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "korridor/laravel-model-validation-rules": "^3.0", "laravel/framework": "^12.19.3", "laravel/jetstream": "^5.0", + "laravel/nightwatch": "^1.11", "laravel/octane": "^2.3", "laravel/passport": "^13.0.5", "laravel/tinker": "^2.8", @@ -118,7 +119,8 @@ "extra": { "laravel": { "dont-discover": [ - "laravel/telescope" + "laravel/telescope", + "nwidart/laravel-modules" ] } }, diff --git a/composer.lock b/composer.lock index fbb7e06c..bf0df564 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": "54a147662be6cf2e0729cb27b781cb6e", + "content-hash": "06d62c6b4e16a4464716e10797720d78", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -4485,6 +4485,92 @@ }, "time": "2025-06-16T13:27:00+00:00" }, + { + "name": "laravel/nightwatch", + "version": "v1.11.5", + "source": { + "type": "git", + "url": "https://github.com/laravel/nightwatch.git", + "reference": "0fa3864d14331da1f8b7a4bb576937b6e9ea78f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/nightwatch/zipball/0fa3864d14331da1f8b7a4bb576937b6e9ea78f1", + "reference": "0fa3864d14331da1f8b7a4bb576937b6e9ea78f1", + "shasum": "" + }, + "require": { + "ext-zlib": "*", + "guzzlehttp/promises": "^2.0", + "laravel/framework": "^10.0|^11.0|^12.0", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^2.0|^3.0", + "php": "^8.2", + "psr/http-message": "^1.0|^2.0", + "psr/log": "^1.0|^2.0|^3.0", + "ramsey/uuid": "^4.0", + "symfony/console": "^6.0|^7.0", + "symfony/http-foundation": "^6.0|^7.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.349", + "ext-pdo": "*", + "guzzlehttp/guzzle": "^7.0", + "guzzlehttp/psr7": "^2.0", + "laravel/horizon": "^5.4", + "laravel/pint": "1.21.0", + "laravel/vapor-core": "^2.38.2", + "livewire/livewire": "^2.0|^3.0", + "mockery/mockery": "^1.0", + "mongodb/laravel-mongodb": "^4.0|^5.0", + "orchestra/testbench": "^8.0|^9.0|^10.0", + "orchestra/testbench-core": "^8.0|^9.0|^10.0", + "orchestra/workbench": "^8.0|^9.0|^10.0", + "phpstan/phpstan": "^1.0", + "phpunit/phpunit": "^10.0|^11.0", + "singlestoredb/singlestoredb-laravel": "^1.0|^2.0", + "spatie/laravel-ignition": "^2.0", + "symfony/mailer": "^6.0|^7.0", + "symfony/mime": "^6.0|^7.0", + "symfony/var-dumper": "^6.0|^7.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Nightwatch\\NightwatchServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Nightwatch\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The official Laravel Nightwatch package.", + "homepage": "https://nightwatch.laravel.com", + "keywords": [ + "Insights", + "laravel", + "monitoring" + ], + "support": { + "docs": "https://nightwatch.laravel.com/docs", + "issues": "https://github.com/laravel/nightwatch/issues", + "source": "https://github.com/laravel/nightwatch" + }, + "time": "2025-08-04T07:34:08+00:00" + }, { "name": "laravel/octane", "version": "v2.11.0", diff --git a/config/app.php b/config/app.php index 8acf6252..a15c3759 100644 --- a/config/app.php +++ b/config/app.php @@ -9,6 +9,7 @@ use App\Enums\TimeFormat; use Illuminate\Support\Facades\Facade; use Illuminate\Support\ServiceProvider; +use Nwidart\Modules\LaravelModulesServiceProvider; return [ @@ -197,6 +198,7 @@ App\Providers\FortifyServiceProvider::class, App\Providers\JetstreamServiceProvider::class, // Warning: Do not add TelescopeServiceProvider here since it is already conditionally registered in AppServiceProvider + LaravelModulesServiceProvider::class, ])->toArray(), /* diff --git a/config/database.php b/config/database.php index e2fcc2cd..38e784b3 100644 --- a/config/database.php +++ b/config/database.php @@ -96,12 +96,11 @@ 'driver' => 'pgsql', 'url' => env('DATABASE_URL'), 'read' => [ - 'host' => env('DB_READ_HOST', env('DB_HOST')), + 'host' => env('DB_READ_HOST', env('DB_HOST', '127.0.0.1')), ], 'write' => [ - 'host' => env('DB_WRITE_HOST', env('DB_HOST')), + 'host' => env('DB_WRITE_HOST', env('DB_HOST', '127.0.0.1')), ], - // 'host' => env('DB_TEST_HOST', '127.0.0.1'), 'port' => env('DB_TEST_PORT', '5432'), 'database' => env('DB_TEST_DATABASE', 'forge'), 'username' => env('DB_TEST_USERNAME', 'forge'), diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 864869a5..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,211 +0,0 @@ -services: - laravel.test: - build: - context: ./docker/local/8.3 - dockerfile: Dockerfile - args: - WWWGROUP: '${WWWGROUP}' - ports: - - '${FORWARD_WEB_PORT:-8083}:80' - image: sail-8.3/app - labels: - - "traefik.enable=true" - - "traefik.docker.network=${NETWORK_NAME}" - - "traefik.http.services.solidtime-dev.loadbalancer.server.port=80" - - "traefik.http.routers.solidtime-dev.rule=Host(`${NGINX_HOST_NAME}`)" - - "traefik.http.routers.solidtime-dev.entrypoints=web" - - "traefik.http.routers.solidtime-dev.service=solidtime-dev" - - "traefik.http.routers.solidtime-dev-https.rule=Host(`${NGINX_HOST_NAME}`)" - - "traefik.http.routers.solidtime-dev-https.service=solidtime-dev" - - "traefik.http.routers.solidtime-dev-https.entrypoints=websecure" - - "traefik.http.routers.solidtime-dev-https.tls=true" - # vite - - "traefik.http.services.solidtime-dev-vite.loadbalancer.server.port=5173" - # http - - "traefik.http.routers.solidtime-dev-vite.rule=Host(`${VITE_HOST_NAME}`)" - - "traefik.http.routers.solidtime-dev-vite.service=solidtime-dev-vite" - - "traefik.http.routers.solidtime-dev-vite.entrypoints=web" - extra_hosts: - - "host.docker.internal:host-gateway" - - "storage.${NGINX_HOST_NAME}:${REVERSE_PROXY_IP:-10.100.100.10}" - environment: - XDG_CONFIG_HOME: /var/www/html/config - XDG_DATA_HOME: /var/www/html/data - WWWUSER: '${WWWUSER}' - LARAVEL_SAIL: 1 - XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}' - XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}' - IGNITION_LOCAL_SITES_PATH: '${PWD}' - VITE_HOST_NAME: '${VITE_HOST_NAME}' - volumes: - - '.:/var/www/html' - networks: - - sail - - reverse-proxy - depends_on: - - pgsql - pgsql: - image: 'postgres:15' - ports: - - '${FORWARD_DB_PORT:-5432}:5432' - environment: - PGPASSWORD: '${DB_PASSWORD:-secret}' - POSTGRES_DB: '${DB_DATABASE}' - POSTGRES_USER: '${DB_USERNAME}' - POSTGRES_PASSWORD: '${DB_PASSWORD:-secret}' - volumes: - - 'sail-pgsql:/var/lib/postgresql/data' - - './docker/local/pgsql/create-testing-database.sql:/docker-entrypoint-initdb.d/10-create-testing-database.sql' - networks: - - sail - healthcheck: - test: - - CMD - - pg_isready - - '-q' - - '-d' - - '${DB_DATABASE}' - - '-U' - - '${DB_USERNAME}' - retries: 3 - timeout: 5s - pgsql_test: - image: 'postgres:15' - environment: - PGPASSWORD: '${DB_PASSWORD:-secret}' - POSTGRES_DB: '${DB_DATABASE}' - POSTGRES_USER: '${DB_USERNAME}' - POSTGRES_PASSWORD: '${DB_PASSWORD:-secret}' - volumes: - - 'sail-pgsql-test:/var/lib/postgresql/data' - - './docker/local/pgsql/create-testing-database.sql:/docker-entrypoint-initdb.d/10-create-testing-database.sql' - networks: - - sail - healthcheck: - test: - - CMD - - pg_isready - - '-q' - - '-d' - - '${DB_DATABASE}' - - '-U' - - '${DB_USERNAME}' - retries: 3 - timeout: 5s - mailpit: - image: 'axllent/mailpit:latest' - labels: - - "traefik.enable=true" - - "traefik.docker.network=${NETWORK_NAME}" - - "traefik.http.services.solidtime-dev-mailpit.loadbalancer.server.port=8025" - - "traefik.http.routers.solidtime-dev-mailpit.rule=Host(`mail.${NGINX_HOST_NAME}`)" - - "traefik.http.routers.solidtime-dev-mailpit.entrypoints=web" - - "traefik.http.routers.solidtime-dev-mailpit.service=solidtime-dev-mailpit" - - "traefik.http.routers.solidtime-dev-mailpit-https.rule=Host(`mail.${NGINX_HOST_NAME}`)" - - "traefik.http.routers.solidtime-dev-mailpit-https.service=solidtime-dev-mailpit" - - "traefik.http.routers.solidtime-dev-mailpit-https.entrypoints=websecure" - - "traefik.http.routers.solidtime-dev-mailpit-https.tls=true" - networks: - - sail - - reverse-proxy - playwright: - image: mcr.microsoft.com/playwright:v1.51.1-jammy - command: ['npx', 'playwright', 'test', '--ui-port=8080', '--ui-host=0.0.0.0'] - working_dir: /src - extra_hosts: - - "${NGINX_HOST_NAME}:${REVERSE_PROXY_IP:-10.100.100.10}" - - "${VITE_HOST_NAME}:${REVERSE_PROXY_IP:-10.100.100.10}" - labels: - - "traefik.enable=true" - - "traefik.docker.network=${NETWORK_NAME}" - - "traefik.http.services.solidtime-dev-playwright.loadbalancer.server.port=8080" - - "traefik.http.routers.solidtime-dev-playwright.rule=Host(`playwright.${NGINX_HOST_NAME}`)" - - "traefik.http.routers.solidtime-dev-playwright.entrypoints=web" - - "traefik.http.routers.solidtime-dev-playwright-https.rule=Host(`playwright.${NGINX_HOST_NAME}`)" - - "traefik.http.routers.solidtime-dev-playwright-https.entrypoints=websecure" - - "traefik.http.routers.solidtime-dev-playwright-https.tls=true" - networks: - - sail - - reverse-proxy - volumes: - - '.:/src' - minio: - image: 'minio/minio:latest' - environment: - MINIO_BROWSER_REDIRECT_URL: 'https://storage-management.${NGINX_HOST_NAME}' - MINIO_ROOT_USER: 'sail' - MINIO_ROOT_PASSWORD: 'password' - volumes: - - 'sail-minio:/data/minio' - networks: - - reverse-proxy - - sail - command: minio server /data/minio --console-address ":8900" - healthcheck: - test: [ "CMD", "mc", "ready", "local" ] - interval: 5s - timeout: 5s - retries: 5 - labels: - - "traefik.enable=true" - - "traefik.docker.network=${NETWORK_NAME}" - # Storage Frontend - - "traefik.http.services.solidtime-dev-storage-frontend.loadbalancer.server.port=9000" - # http - - "traefik.http.routers.solidtime-dev-storage-frontend.rule=Host(`storage.${NGINX_HOST_NAME}`)" - - "traefik.http.routers.solidtime-dev-storage-frontend.service=solidtime-dev-storage-frontend" - - "traefik.http.routers.solidtime-dev-storage-frontend.entrypoints=web" - # https - - "traefik.http.routers.solidtime-dev-storage-frontend-https.rule=Host(`storage.${NGINX_HOST_NAME}`)" - - "traefik.http.routers.solidtime-dev-storage-frontend-https.service=solidtime-dev-storage-frontend" - - "traefik.http.routers.solidtime-dev-storage-frontend-https.entrypoints=websecure" - - "traefik.http.routers.solidtime-dev-storage-frontend-https.tls=true" - # Storage Management - - "traefik.http.services.solidtime-dev-storage-management.loadbalancer.server.port=8900" - # http - - "traefik.http.routers.solidtime-dev-storage-management.rule=Host(`storage-management.${NGINX_HOST_NAME}`)" - - "traefik.http.routers.solidtime-dev-storage-management.service=solidtime-dev-storage-management" - - "traefik.http.routers.solidtime-dev-storage-management.entrypoints=web" - # https - - "traefik.http.routers.solidtime-dev-storage-management-https.rule=Host(`storage-management.${NGINX_HOST_NAME}`)" - - "traefik.http.routers.solidtime-dev-storage-management-https.service=solidtime-dev-storage-management" - - "traefik.http.routers.solidtime-dev-storage-management-https.entrypoints=websecure" - - "traefik.http.routers.solidtime-dev-storage-management-https.tls=true" - - minio-create-bucket: - image: minio/mc:latest - depends_on: - - minio - environment: - S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID} - S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY} - S3_BUCKET: ${S3_BUCKET} - S3_ENDPOINT: ${S3_ENDPOINT} - volumes: - - './docker/local/minio:/etc/minio' - networks: - - sail - - reverse-proxy - entrypoint: /etc/minio/create_bucket.sh - extra_hosts: - - "storage.${NGINX_HOST_NAME}:${REVERSE_PROXY_IP:-10.100.100.10}" - - gotenberg: - image: gotenberg/gotenberg:8 - networks: - - sail - healthcheck: - test: ["CMD", "curl", "--silent", "--fail", "http://localhost:3000/health"] -networks: - reverse-proxy: - name: "${NETWORK_NAME}" - external: true - sail: - driver: bridge -volumes: - sail-pgsql: - driver: local - sail-pgsql-test: - driver: local - sail-minio: - driver: local diff --git a/docker/local/8.3/Dockerfile b/docker/local/8.3/Dockerfile deleted file mode 100644 index b2a1f39c..00000000 --- a/docker/local/8.3/Dockerfile +++ /dev/null @@ -1,65 +0,0 @@ -FROM ubuntu:22.04 - -LABEL maintainer="Taylor Otwell" - -ARG WWWGROUP -ARG NODE_VERSION=20 -ARG POSTGRES_VERSION=15 - -WORKDIR /var/www/html - -ENV DEBIAN_FRONTEND noninteractive -ENV TZ=UTC -ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80" -ENV SUPERVISOR_PHP_USER="sail" - -RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone - -RUN apt-get update \ - && mkdir -p /etc/apt/keyrings \ - && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python2 dnsutils librsvg2-bin fswatch ffmpeg \ - && curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \ - && echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu jammy main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \ - && apt-get update \ - && apt-get install -y php8.3-cli php8.3-dev \ - php8.3-pgsql php8.3-sqlite3 php8.3-gd \ - php8.3-curl \ - php8.3-imap php8.3-mysql php8.3-mbstring \ - php8.3-xml php8.3-zip php8.3-bcmath php8.3-soap \ - php8.3-intl php8.3-readline \ - php8.3-ldap \ - php8.3-msgpack php8.3-igbinary php8.3-redis php8.3-swoole \ - php8.3-memcached php8.3-pcov php8.3-imagick php8.3-xdebug \ - && curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \ - && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ - && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \ - && apt-get update \ - && apt-get install -y nodejs \ - && npm install -g npm \ - && npm install -g pnpm \ - && npm install -g bun \ - && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \ - && echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ - && curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \ - && echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt jammy-pgdg main" > /etc/apt/sources.list.d/pgdg.list \ - && apt-get update \ - && apt-get install -y yarn \ - && apt-get install -y mysql-client \ - && apt-get install -y postgresql-client-$POSTGRES_VERSION \ - && apt-get -y autoremove \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* - -RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.3 - -RUN groupadd --force -g $WWWGROUP sail -RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail - -COPY start-container /usr/local/bin/start-container -COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf -COPY php.ini /etc/php/8.3/cli/conf.d/99-sail.ini -RUN chmod +x /usr/local/bin/start-container - -EXPOSE 8000 - -ENTRYPOINT ["start-container"] diff --git a/docker/local/8.3/php.ini b/docker/local/8.3/php.ini deleted file mode 100644 index 0320d71c..00000000 --- a/docker/local/8.3/php.ini +++ /dev/null @@ -1,8 +0,0 @@ -[PHP] -post_max_size = 100M -upload_max_filesize = 100M -variables_order = EGPCS -pcov.directory = . - -[opcache] -opcache.enable_cli=1 diff --git a/docker/local/8.3/start-container b/docker/local/8.3/start-container deleted file mode 100644 index 40c55dfe..00000000 --- a/docker/local/8.3/start-container +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then - echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'." - exit 1 -fi - -if [ ! -z "$WWWUSER" ]; then - usermod -u $WWWUSER sail -fi - -if [ ! -d /.composer ]; then - mkdir /.composer -fi - -chmod -R ugo+rw /.composer - -if [ $# -gt 0 ]; then - if [ "$SUPERVISOR_PHP_USER" = "root" ]; then - exec "$@" - else - exec gosu $WWWUSER "$@" - fi -else - exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf -fi diff --git a/docker/local/8.3/supervisord.conf b/docker/local/8.3/supervisord.conf deleted file mode 100644 index 656da8a9..00000000 --- a/docker/local/8.3/supervisord.conf +++ /dev/null @@ -1,14 +0,0 @@ -[supervisord] -nodaemon=true -user=root -logfile=/var/log/supervisor/supervisord.log -pidfile=/var/run/supervisord.pid - -[program:php] -command=%(ENV_SUPERVISOR_PHP_COMMAND)s -user=%(ENV_SUPERVISOR_PHP_USER)s -environment=LARAVEL_SAIL="1" -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 diff --git a/docker/local/minio/create_bucket.sh b/docker/local/minio/create_bucket.sh deleted file mode 100755 index 944a2520..00000000 --- a/docker/local/minio/create_bucket.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh - -# Source: https://helgesver.re/articles/laravel-sail-create-minio-bucket-automatically - -/usr/bin/mc config host add local ${S3_ENDPOINT} ${S3_ACCESS_KEY_ID} ${S3_SECRET_ACCESS_KEY}; -/usr/bin/mc rm -r --force local/${S3_BUCKET}; -/usr/bin/mc mb --ignore-existing local/${S3_BUCKET}; -/usr/bin/mc anonymous set public local/${S3_BUCKET}; - -exit 0; diff --git a/docker/local/pgsql/create-testing-database.sql b/docker/local/pgsql/create-testing-database.sql deleted file mode 100644 index d84dc07b..00000000 --- a/docker/local/pgsql/create-testing-database.sql +++ /dev/null @@ -1,2 +0,0 @@ -SELECT 'CREATE DATABASE testing' -WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'testing')\gexec diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile deleted file mode 100644 index f414b057..00000000 --- a/docker/prod/Dockerfile +++ /dev/null @@ -1,214 +0,0 @@ -ARG PHP_VERSION=8.3 -ARG FRANKENPHP_VERSION=1.8 -ARG COMPOSER_VERSION=2.8 -ARG BUN_VERSION="latest" -ARG APP_ENV -ARG DOCKER_FILES_BASE_PATH="docker/prod/" - -FROM composer:${COMPOSER_VERSION} AS vendor - -FROM dunglas/frankenphp:${FRANKENPHP_VERSION}-builder-php${PHP_VERSION} AS upstream - -COPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy - -RUN CGO_ENABLED=1 \ - XCADDY_SETCAP=1 \ - XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \ - CGO_CFLAGS=$(php-config --includes) \ - CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \ - xcaddy build \ - --output /usr/local/bin/frankenphp \ - --with github.com/dunglas/frankenphp=./ \ - --with github.com/dunglas/frankenphp/caddy=./caddy/ \ - --with github.com/dunglas/caddy-cbrotli - -FROM dunglas/frankenphp:${FRANKENPHP_VERSION}-php${PHP_VERSION} AS base - -COPY --from=upstream /usr/local/bin/frankenphp /usr/local/bin/frankenphp - -LABEL maintainer="solidtime " -LABEL org.opencontainers.image.title="solidtime" -LABEL org.opencontainers.image.description="solidtime is a modern open source timetracker for freelancers and agencies" -LABEL org.opencontainers.image.source="https://github.com/solidtime-io/solidtime" -LABEL org.opencontainers.image.licenses="AGPL" - -ARG WWWUSER=1000 -ARG WWWGROUP=1000 -ARG TZ=UTC -ARG APP_DIR=/var/www/html -ARG APP_ENV -ARG APP_HOST -ARG DOCKER_FILES_BASE_PATH - -ENV DEBIAN_FRONTEND=noninteractive \ - TERM=xterm-color \ - OCTANE_SERVER=frankenphp \ - TZ=${TZ} \ - USER=octane \ - ROOT=${APP_DIR} \ - APP_ENV=${APP_ENV} \ - COMPOSER_FUND=0 \ - COMPOSER_MAX_PARALLEL_HTTP=24 \ - XDG_CONFIG_HOME=${APP_DIR}/.config \ - XDG_DATA_HOME=${APP_DIR}/.data \ - SERVER_NAME=${APP_HOST} - -WORKDIR ${ROOT} - -SHELL ["/bin/bash", "-eou", "pipefail", "-c"] - -RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime \ - && echo ${TZ} > /etc/timezone - -RUN apt-get update; \ - apt-get upgrade -yqq; \ - apt-get install -yqq --no-install-recommends --show-progress \ - apt-utils \ - curl \ - wget \ - vim \ - git \ - ncdu \ - procps \ - unzip \ - ca-certificates \ - supervisor \ - libsodium-dev \ - libbrotli-dev \ - # Install PHP extensions (included with dunglas/frankenphp) - && install-php-extensions \ - bz2 \ - pcntl \ - mbstring \ - bcmath \ - sockets \ - pgsql \ - pdo_pgsql \ - opcache \ - exif \ - pdo_mysql \ - zip \ - uv \ - vips \ - intl \ - gd \ - redis \ - rdkafka \ - memcached \ - igbinary \ - ldap \ - && apt-get -y autoremove \ - && apt-get clean \ - && docker-php-source delete \ - && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ - && rm /var/log/lastlog /var/log/faillog - -RUN arch="$(uname -m)" \ - && case "$arch" in \ - armhf) _cronic_fname='supercronic-linux-arm' ;; \ - aarch64) _cronic_fname='supercronic-linux-arm64' ;; \ - x86_64) _cronic_fname='supercronic-linux-amd64' ;; \ - x86) _cronic_fname='supercronic-linux-386' ;; \ - *) echo >&2 "error: unsupported architecture: $arch"; exit 1 ;; \ - esac \ - && wget -q "https://github.com/aptible/supercronic/releases/download/v0.2.29/${_cronic_fname}" \ - -O /usr/bin/supercronic \ - && chmod +x /usr/bin/supercronic \ - && mkdir -p /etc/supercronic \ - && echo "*/1 * * * * php ${ROOT}/artisan schedule:run --no-interaction" > /etc/supercronic/laravel - -RUN userdel --remove --force www-data \ - && groupadd --force -g ${WWWGROUP} ${USER} \ - && useradd -ms /bin/bash --no-log-init --no-user-group -g ${WWWGROUP} -u ${WWWUSER} ${USER} \ - && setcap -r /usr/local/bin/frankenphp - -RUN chown -R ${USER}:${USER} ${ROOT} /var/{log,run} \ - && chmod -R a+rw ${ROOT} /var/{log,run} - -RUN cp ${PHP_INI_DIR}/php.ini-production ${PHP_INI_DIR}/php.ini - -USER ${USER} - -COPY --link --chown=${WWWUSER}:${WWWUSER} --from=vendor /usr/bin/composer /usr/bin/composer - -COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/supervisord.conf /etc/ -COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/octane/FrankenPHP/supervisord.frankenphp.conf /etc/supervisor/conf.d/ -COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/supervisord.*.conf /etc/supervisor/conf.d/ -COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/start-container /usr/local/bin/start-container -COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/healthcheck /usr/local/bin/healthcheck -COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/php.ini ${PHP_INI_DIR}/conf.d/99-octane.ini - -RUN chmod +x /usr/local/bin/start-container /usr/local/bin/healthcheck - -########################################### - -#FROM base AS common -# -#USER ${USER} -# -#COPY --link --chown=${WWWUSER}:${WWWUSER} . . -# -#RUN composer install \ -# --no-dev \ -# --no-interaction \ -# --no-autoloader \ -# --no-ansi \ -# --no-scripts \ -# --audit - -########################################### -# Build frontend assets with Bun -########################################### - -#FROM oven/bun:${BUN_VERSION} AS build -# -#ARG APP_ENV -# -#ENV ROOT=/var/www/html \ -# APP_ENV=${APP_ENV} \ -# NODE_ENV=${APP_ENV:-production} -# -#WORKDIR ${ROOT} -# -#COPY --link package.json bun.lock* ./ -# -#RUN bun install --frozen-lockfile -# -#COPY --link . . -#COPY --link --from=common ${ROOT}/vendor vendor -# -#RUN bun run build - -########################################### - -#FROM common AS runner - -USER ${USER} - -ENV WITH_HORIZON=false \ - WITH_SCHEDULER=false \ - WITH_REVERB=false - -COPY --link --chown=${WWWUSER}:${WWWUSER} . . -#COPY --link --chown=${WWWUSER}:${WWWUSER} --from=build ${ROOT}/public public - -RUN mkdir -p \ - storage/framework/{sessions,views,cache,testing} \ - storage/logs \ - bootstrap/cache && chmod -R a+rw storage - -#RUN composer install \ -# --classmap-authoritative \ -# --no-interaction \ -# --no-ansi \ -# --no-dev \ -# && composer clear-cache - -RUN cat .env -#RUN php artisan env - -EXPOSE 8000 - -ENTRYPOINT ["start-container"] - -#HEALTHCHECK --start-period=5s --interval=2s --timeout=5s --retries=8 CMD healthcheck || exit 1 diff --git a/docker/prod/LICENSE b/docker/prod/LICENSE deleted file mode 100644 index 96aa7dde..00000000 --- a/docker/prod/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 Exa Company - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/docker/prod/deployment/healthcheck b/docker/prod/deployment/healthcheck deleted file mode 100644 index c4281814..00000000 --- a/docker/prod/deployment/healthcheck +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env sh - -set -e - -container_mode=${CONTAINER_MODE:-"http"} - -if [ "${container_mode}" = "http" ]; then - php "${ROOT}/artisan" octane:status -elif [ "${container_mode}" = "horizon" ]; then - php "${ROOT}/artisan" horizon:status -elif [ "${container_mode}" = "scheduler" ]; then - if [ "$(supervisorctl status scheduler:scheduler_0 | awk '{print tolower($2)}')" = "running" ]; then - exit 0 - else - echo "Healthcheck failed." - exit 1 - fi -elif [ "${container_mode}" = "reverb" ]; then - if [ "$(supervisorctl status reverb:reverb_0 | awk '{print tolower($2)}')" = "running" ]; then - exit 0 - else - echo "Healthcheck failed." - exit 1 - fi -elif [ "${container_mode}" = "worker" ]; then - if [ "$(supervisorctl status worker:worker_0 | awk '{print tolower($2)}')" = "running" ]; then - exit 0 - else - echo "Healthcheck failed." - exit 1 - fi -else - echo "Container mode mismatched." - exit 1 -fi diff --git a/docker/prod/deployment/octane/FrankenPHP/Caddyfile b/docker/prod/deployment/octane/FrankenPHP/Caddyfile deleted file mode 100644 index 08211d09..00000000 --- a/docker/prod/deployment/octane/FrankenPHP/Caddyfile +++ /dev/null @@ -1,68 +0,0 @@ -{ - {$CADDY_GLOBAL_OPTIONS} - - admin {$CADDY_SERVER_ADMIN_HOST}:{$CADDY_SERVER_ADMIN_PORT} - - frankenphp { - worker "{$APP_PUBLIC_PATH}/frankenphp-worker.php" {$CADDY_SERVER_WORKER_COUNT} - } - - metrics { - per_host - } - - servers { - protocols h1 - } -} - -{$CADDY_EXTRA_CONFIG} - -{$CADDY_SERVER_SERVER_NAME} { - log { - level WARN - - format filter { - wrap {$CADDY_SERVER_LOGGER} - fields { - uri query { - replace authorization REDACTED - } - } - } - } - - route { - root * "{$APP_PUBLIC_PATH}" - encode zstd br gzip - - {$CADDY_SERVER_EXTRA_DIRECTIVES} - - request_body { - max_size 500MB - } - - @static { - file - path *.js *.css *.jpg *.jpeg *.webp *.weba *.webm *.gif *.png *.ico *.cur *.gz *.svg *.svgz *.mp4 *.mp3 *.ogg *.ogv *.htc *.woff2 *.woff - } - - @staticshort { - file - path *.json *.xml *.rss - } - - header @static Cache-Control "public, immutable, stale-while-revalidate, max-age=31536000" - - header @staticshort Cache-Control "no-cache, max-age=3600" - - @rejected `path('*.bak', '*.conf', '*.dist', '*.fla', '*.ini', '*.inc', '*.inci', '*.log', '*.orig', '*.psd', '*.sh', '*.sql', '*.swo', '*.swp', '*.swop', '*/.*') && !path('*/.well-known/*')` - error @rejected 401 - - php_server { - index frankenphp-worker.php - try_files {path} frankenphp-worker.php - resolve_root_symlink - } - } -} diff --git a/docker/prod/deployment/octane/FrankenPHP/supervisord.frankenphp.conf b/docker/prod/deployment/octane/FrankenPHP/supervisord.frankenphp.conf deleted file mode 100644 index 20dbc3ee..00000000 --- a/docker/prod/deployment/octane/FrankenPHP/supervisord.frankenphp.conf +++ /dev/null @@ -1,65 +0,0 @@ -[program:octane] -process_name = %(program_name)s_%(process_num)s -command = php %(ENV_ROOT)s/artisan octane:frankenphp --host=0.0.0.0 --port=8000 --admin-port=2019 --caddyfile=%(ENV_ROOT)s/docker/prod/deployment/octane/FrankenPHP/Caddyfile -user = %(ENV_USER)s -priority = 1 -autostart = true -autorestart = true -environment = LARAVEL_OCTANE = "1" -stdout_logfile = /dev/stdout -stdout_logfile_maxbytes = 0 -stderr_logfile = /dev/stderr -stderr_logfile_maxbytes = 0 - -[program:horizon] -process_name = %(program_name)s_%(process_num)s -command = php %(ENV_ROOT)s/artisan horizon -user = %(ENV_USER)s -priority = 3 -autostart = %(ENV_WITH_HORIZON)s -autorestart = true -stdout_logfile = %(ENV_ROOT)s/storage/logs/horizon.log -stdout_logfile_maxbytes = 200MB -stderr_logfile = %(ENV_ROOT)s/storage/logs/horizon.log -stderr_logfile_maxbytes = 200MB -stopwaitsecs = 3600 - -[program:scheduler] -process_name = %(program_name)s_%(process_num)s -command = supercronic -overlapping /etc/supercronic/laravel -user = %(ENV_USER)s -autostart = %(ENV_WITH_SCHEDULER)s -autorestart = true -stdout_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log -stdout_logfile_maxbytes = 200MB -stderr_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log -stderr_logfile_maxbytes = 200MB - -[program:clear-scheduler-cache] -process_name = %(program_name)s_%(process_num)s -command = php %(ENV_ROOT)s/artisan schedule:clear-cache -user = %(ENV_USER)s -autostart = %(ENV_WITH_SCHEDULER)s -autorestart = false -startsecs = 0 -startretries = 1 -stdout_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log -stdout_logfile_maxbytes = 200MB -stderr_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log -stderr_logfile_maxbytes = 200MB - -[program:reverb] -process_name = %(program_name)s_%(process_num)s -command = php %(ENV_ROOT)s/artisan reverb:start -user = %(ENV_USER)s -priority = 2 -autostart = %(ENV_WITH_REVERB)s -autorestart = true -stdout_logfile = %(ENV_ROOT)s/storage/logs/reverb.log -stdout_logfile_maxbytes = 200MB -stderr_logfile = %(ENV_ROOT)s/storage/logs/reverb.log -stderr_logfile_maxbytes = 200MB -minfds = 10000 - -[include] -files = /etc/supervisord.conf diff --git a/docker/prod/deployment/php.ini b/docker/prod/deployment/php.ini deleted file mode 100644 index d770d10b..00000000 --- a/docker/prod/deployment/php.ini +++ /dev/null @@ -1,31 +0,0 @@ -[PHP] -post_max_size = 100M -upload_max_filesize = 100M -expose_php = 0 -realpath_cache_size = 16M -realpath_cache_ttl = 360 -max_input_time = 5 -register_argc_argv = 0 -date.timezone = ${TZ:-UTC} - -[Opcache] -opcache.enable = 1 -opcache.enable_cli = 1 -opcache.memory_consumption = 256M -opcache.use_cwd = 0 -opcache.max_file_size = 0 -opcache.max_accelerated_files = 32531 -opcache.validate_timestamps = 0 -opcache.file_update_protection = 0 -opcache.interned_strings_buffer = 16 - -[JIT] -opcache.jit_buffer_size = 128M -opcache.jit = function -opcache.jit_prof_threshold = 0.001 -opcache.jit_max_root_traces = 2048 -opcache.jit_max_side_traces = 256 - -[zlib] -zlib.output_compression = On -zlib.output_compression_level = 9 diff --git a/docker/prod/deployment/start-container b/docker/prod/deployment/start-container deleted file mode 100644 index 0fb3fcdb..00000000 --- a/docker/prod/deployment/start-container +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env sh -set -e - -container_mode=${CONTAINER_MODE:-"http"} -octane_server=${OCTANE_SERVER} -auto_db_migrate=${AUTO_DB_MIGRATE:-false} - -initialStuff() { - echo "Container mode: $container_mode" - - if [ ${auto_db_migrate} = "true" ]; then - echo "Auto database migration enabled." - php artisan migrate --isolated --force - fi - - php artisan storage:link; \ - php artisan optimize:clear; \ - php artisan optimize; -} - -if [ "$1" != "" ]; then - exec "$@" -elif [ "${container_mode}" = "http" ]; then - initialStuff - echo "Octane Server: $octane_server" - if [ "${octane_server}" = "frankenphp" ]; then - exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.frankenphp.conf - elif [ "${octane_server}" = "swoole" ]; then - exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.swoole.conf - elif [ "${octane_server}" = "roadrunner" ]; then - exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.roadrunner.conf - else - echo "Invalid Octane server supplied." - exit 1 - fi -elif [ "${container_mode}" = "horizon" ]; then - initialStuff - exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.horizon.conf -elif [ "${container_mode}" = "reverb" ]; then - initialStuff - exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.reverb.conf -elif [ "${container_mode}" = "scheduler" ]; then - initialStuff - exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.scheduler.conf -elif [ "${container_mode}" = "worker" ]; then - if [ -z "${WORKER_COMMAND}" ]; then - echo "WORKER_COMMAND is undefined." - exit 1 - fi - initialStuff - exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.worker.conf -else - echo "Container mode mismatched." - exit 1 -fi diff --git a/docker/prod/deployment/supervisord.conf b/docker/prod/deployment/supervisord.conf deleted file mode 100644 index 80d8cdcd..00000000 --- a/docker/prod/deployment/supervisord.conf +++ /dev/null @@ -1,13 +0,0 @@ -[supervisord] -nodaemon = true -user = %(ENV_USER)s -logfile = /var/log/supervisor/supervisord.log -pidfile = /var/run/supervisord.pid - -[supervisorctl] - -[inet_http_server] -port = 127.0.0.1:9001 - -[rpcinterface:supervisor] -supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface diff --git a/docker/prod/deployment/supervisord.horizon.conf b/docker/prod/deployment/supervisord.horizon.conf deleted file mode 100644 index c57062c6..00000000 --- a/docker/prod/deployment/supervisord.horizon.conf +++ /dev/null @@ -1,14 +0,0 @@ -[program:horizon] -process_name = %(program_name)s_%(process_num)s -command = php %(ENV_ROOT)s/artisan horizon -user = %(ENV_USER)s -autostart = true -autorestart = true -stdout_logfile = /dev/stdout -stdout_logfile_maxbytes = 0 -stderr_logfile = /dev/stderr -stderr_logfile_maxbytes = 0 -stopwaitsecs = 3600 - -[include] -files = /etc/supervisord.conf diff --git a/docker/prod/deployment/supervisord.reverb.conf b/docker/prod/deployment/supervisord.reverb.conf deleted file mode 100644 index 6e540eb1..00000000 --- a/docker/prod/deployment/supervisord.reverb.conf +++ /dev/null @@ -1,14 +0,0 @@ -[program:reverb] -process_name = %(program_name)s_%(process_num)s -command = php %(ENV_ROOT)s/artisan reverb:start -user = %(ENV_USER)s -autostart = true -autorestart = true -stdout_logfile = /dev/stdout -stdout_logfile_maxbytes = 0 -stderr_logfile = /dev/stderr -stderr_logfile_maxbytes = 0 -minfds = 10000 - -[include] -files = /etc/supervisord.conf diff --git a/docker/prod/deployment/supervisord.scheduler.conf b/docker/prod/deployment/supervisord.scheduler.conf deleted file mode 100644 index 20722be5..00000000 --- a/docker/prod/deployment/supervisord.scheduler.conf +++ /dev/null @@ -1,26 +0,0 @@ -[program:scheduler] -process_name = %(program_name)s_%(process_num)s -command = supercronic -overlapping /etc/supercronic/laravel -user = %(ENV_USER)s -autostart = true -autorestart = true -stdout_logfile = /dev/stdout -stdout_logfile_maxbytes = 0 -stderr_logfile = /dev/stderr -stderr_logfile_maxbytes = 0 - -[program:clear-scheduler-cache] -process_name = %(program_name)s_%(process_num)s -command = php %(ENV_ROOT)s/artisan schedule:clear-cache -user = %(ENV_USER)s -autostart = true -autorestart = false -startsecs = 0 -startretries = 1 -stdout_logfile = /dev/stdout -stdout_logfile_maxbytes = 0 -stderr_logfile = /dev/stderr -stderr_logfile_maxbytes = 0 - -[include] -files = /etc/supervisord.conf diff --git a/docker/prod/deployment/supervisord.worker.conf b/docker/prod/deployment/supervisord.worker.conf deleted file mode 100644 index 0d939c09..00000000 --- a/docker/prod/deployment/supervisord.worker.conf +++ /dev/null @@ -1,13 +0,0 @@ -[program:worker] -process_name = %(program_name)s_%(process_num)s -command = %(ENV_WORKER_COMMAND)s -user = %(ENV_USER)s -autostart = true -autorestart = true -stdout_logfile = /dev/stdout -stdout_logfile_maxbytes = 0 -stderr_logfile = /dev/stderr -stderr_logfile_maxbytes = 0 - -[include] -files = /etc/supervisord.conf diff --git a/e2e/clients.spec.ts b/e2e/clients.spec.ts index 9ca98d5d..6922b99b 100644 --- a/e2e/clients.spec.ts +++ b/e2e/clients.spec.ts @@ -7,11 +7,8 @@ async function goToProjectsOverview(page: Page) { } // Create new project via modal -test('test that creating and deleting a new client via the modal works', async ({ - page, -}) => { - const newClientName = - 'New Project ' + Math.floor(1 + Math.random() * 10000); +test.skip('test that creating and deleting a new client via the modal works', async ({ page }) => { + const newClientName = 'New Project ' + Math.floor(1 + Math.random() * 10000); await goToProjectsOverview(page); await page.getByRole('button', { name: 'Create Client' }).click(); await page.getByPlaceholder('Client Name').fill(newClientName); @@ -28,13 +25,9 @@ test('test that creating and deleting a new client via the modal works', async ( ]); await expect(page.getByTestId('client_table')).toContainText(newClientName); - const moreButton = page.locator( - "[aria-label='Actions for Client " + newClientName + "']" - ); + const moreButton = page.locator("[aria-label='Actions for Client " + newClientName + "']"); moreButton.click(); - const deleteButton = page.locator( - "[aria-label='Delete Client " + newClientName + "']" - ); + const deleteButton = page.locator("[aria-label='Delete Client " + newClientName + "']"); await Promise.all([ deleteButton.click(), @@ -45,9 +38,7 @@ test('test that creating and deleting a new client via the modal works', async ( response.status() === 204 ), ]); - await expect(page.getByTestId('client_table')).not.toContainText( - newClientName - ); + await expect(page.getByTestId('client_table')).not.toContainText(newClientName); }); test('test that archiving and unarchiving clients works', async ({ page }) => { diff --git a/e2e/members.spec.ts b/e2e/members.spec.ts index 253acf79..70be0e7c 100644 --- a/e2e/members.spec.ts +++ b/e2e/members.spec.ts @@ -22,12 +22,8 @@ test('test that new manager can be invited', async ({ page }) => { await page.getByLabel('Email').fill(`new+${editorId}@editor.test`); await page.getByRole('button', { name: 'Manager' }).click(); await Promise.all([ - page - .getByRole('button', { name: 'Invite Member', exact: true }) - .click(), - expect(page.getByRole('main')).toContainText( - `new+${editorId}@editor.test` - ), + page.getByRole('button', { name: 'Invite Member', exact: true }).click(), + expect(page.getByRole('main')).toContainText(`new+${editorId}@editor.test`), ]); }); @@ -38,12 +34,8 @@ test('test that new employee can be invited', async ({ page }) => { await page.getByLabel('Email').fill(`new+${editorId}@editor.test`); await page.getByRole('button', { name: 'Employee' }).click(); await Promise.all([ - page - .getByRole('button', { name: 'Invite Member', exact: true }) - .click(), - await expect(page.getByRole('main')).toContainText( - `new+${editorId}@editor.test` - ), + page.getByRole('button', { name: 'Invite Member', exact: true }).click(), + await expect(page.getByRole('main')).toContainText(`new+${editorId}@editor.test`), ]); }); @@ -54,12 +46,8 @@ test('test that new admin can be invited', async ({ page }) => { await page.getByLabel('Email').fill(`new+${adminId}@admin.test`); await page.getByRole('button', { name: 'Administrator' }).click(); await Promise.all([ - page - .getByRole('button', { name: 'Invite Member', exact: true }) - .click(), - expect(page.getByRole('main')).toContainText( - `new+${adminId}@admin.test` - ), + page.getByRole('button', { name: 'Invite Member', exact: true }).click(), + expect(page.getByRole('main')).toContainText(`new+${adminId}@admin.test`), ]); }); test('test that error shows if no role is selected', async ({ page }) => { @@ -69,9 +57,7 @@ test('test that error shows if no role is selected', async ({ page }) => { await page.getByLabel('Email').fill(`new+${noRoleId}@norole.test`); await Promise.all([ - page - .getByRole('button', { name: 'Invite Member', exact: true }) - .click(), + page.getByRole('button', { name: 'Invite Member', exact: true }).click(), expect(page.getByText('Please select a role')).toBeVisible(), ]); }); @@ -85,9 +71,7 @@ test('test that organization billable rate can be updated with all existing time await page.getByRole('menuitem').getByText('Edit').click(); await page.getByText('Organization Default Rate').click(); await page.getByText('Custom Rate').click(); - await page - .getByPlaceholder('Billable Rate') - .fill(newBillableRate.toString()); + await page.getByPlaceholder('Billable Rate').fill(newBillableRate.toString()); await page.getByRole('button', { name: 'Update Member' }).click(); await Promise.all([ @@ -103,8 +87,7 @@ test('test that organization billable rate can be updated with all existing time response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && - (await response.json()).data.billable_rate === - newBillableRate * 100 + (await response.json()).data.billable_rate === newBillableRate * 100 ), ]); }); diff --git a/e2e/organization.spec.ts b/e2e/organization.spec.ts index ebb4a539..d2386577 100644 --- a/e2e/organization.spec.ts +++ b/e2e/organization.spec.ts @@ -35,9 +35,9 @@ test('test that organization name can be updated', async ({ page }) => { await page.getByLabel('Organization Name').fill('NEW ORG NAME'); await page.getByLabel('Organization Name').press('Enter'); await page.getByLabel('Organization Name').press('Meta+r'); - await expect( - page.locator('[data-testid="organization_switcher"]:visible') - ).toContainText('NEW ORG NAME'); + await expect(page.locator('[data-testid="organization_switcher"]:visible')).toContainText( + 'NEW ORG NAME' + ); }); test('test that organization billable rate can be updated with all existing time entries', async ({ @@ -46,9 +46,7 @@ test('test that organization billable rate can be updated with all existing time await goToOrganizationSettings(page); const newBillableRate = Math.round(Math.random() * 10000); await page.getByLabel('Organization Billable Rate').click(); - await page - .getByLabel('Organization Billable Rate') - .fill(newBillableRate.toString()); + await page.getByLabel('Organization Billable Rate').fill(newBillableRate.toString()); await page .locator('form') .filter({ hasText: 'Organization Billable' }) @@ -56,9 +54,7 @@ test('test that organization billable rate can be updated with all existing time .click(); await Promise.all([ - page - .getByRole('button', { name: 'Yes, update existing time entries' }) - .click(), + page.getByRole('button', { name: 'Yes, update existing time entries' }).click(), page.waitForRequest( async (request) => request.url().includes('/organizations/') && @@ -70,15 +66,12 @@ test('test that organization billable rate can be updated with all existing time response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && - (await response.json()).data.billable_rate === - newBillableRate * 100 + (await response.json()).data.billable_rate === newBillableRate * 100 ), ]); }); -test('test that organization format settings can be updated', async ({ - page, -}) => { +test('test that organization format settings can be updated', async ({ page }) => { await goToOrganizationSettings(page); // Test number format @@ -113,8 +106,7 @@ test('test that organization format settings can be updated', async ({ response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && - (await response.json()).data.currency_format === - 'iso-code-after-with-space' + (await response.json()).data.currency_format === 'iso-code-after-with-space' ), ]); @@ -132,8 +124,7 @@ test('test that organization format settings can be updated', async ({ response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && - (await response.json()).data.date_format === - 'slash-separated-dd-mm-yyyy' + (await response.json()).data.date_format === 'slash-separated-dd-mm-yyyy' ), ]); @@ -169,19 +160,14 @@ test('test that organization format settings can be updated', async ({ response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && - (await response.json()).data.interval_format === - 'hours-minutes-colon-separated' + (await response.json()).data.interval_format === 'hours-minutes-colon-separated' ), ]); }); -test('test that format settings are reflected in the dashboard', async ({ - page, -}) => { +test('test that format settings are reflected in the dashboard', async ({ page }) => { // check that 0h 00min is displayed - await expect( - page.getByText('0h 00min', { exact: true }).nth(0) - ).toBeVisible(); + await expect(page.getByText('0h 00min', { exact: true }).nth(0)).toBeVisible(); // First set the format settings await goToOrganizationSettings(page); @@ -213,10 +199,8 @@ test('test that format settings are reflected in the dashboard', async ({ response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && - (await response.json()).data.interval_format === - 'hours-minutes-colon-separated' && - (await response.json()).data.currency_format === - 'symbol-after' && + (await response.json()).data.interval_format === 'hours-minutes-colon-separated' && + (await response.json()).data.currency_format === 'symbol-after' && (await response.json()).data.number_format === 'comma-point' ), ]); @@ -232,16 +216,12 @@ test('test that format settings are reflected in the dashboard', async ({ // check that 00:00 is displayed await expect(page.getByText('0:00', { exact: true }).nth(0)).toBeVisible(); // check that 0h 00min is not displayed - await expect( - page.getByText('0h 00min', { exact: true }).nth(0) - ).not.toBeVisible(); + await expect(page.getByText('0h 00min', { exact: true }).nth(0)).not.toBeVisible(); // check that the current date is displayed in the dd/mm/yyyy format on the time page await page.goto(PLAYWRIGHT_BASE_URL + '/time'); await expect( - page - .getByText(new Date().toLocaleDateString('en-GB'), { exact: true }) - .nth(0) + page.getByText(new Date().toLocaleDateString('en-GB'), { exact: true }).nth(0) ).toBeVisible(); }); diff --git a/e2e/profile.spec.ts b/e2e/profile.spec.ts index 2428af9c..a2f34388 100644 --- a/e2e/profile.spec.ts +++ b/e2e/profile.spec.ts @@ -1,34 +1,32 @@ -import {test, expect} from '../playwright/fixtures'; -import {PLAYWRIGHT_BASE_URL} from '../playwright/config'; +import { test, expect } from '../playwright/fixtures'; +import { PLAYWRIGHT_BASE_URL } from '../playwright/config'; -test('test that user name can be updated', async ({page}) => { +test('test that user name can be updated', async ({ page }) => { await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile'); - await page.getByLabel('Name', {exact: true} ).fill('NEW NAME'); + await page.getByLabel('Name', { exact: true }).fill('NEW NAME'); await Promise.all([ - page.getByRole('button', {name: 'Save'}).first().click(), + page.getByRole('button', { name: 'Save' }).first().click(), page.waitForResponse('**/user/profile-information'), ]); await page.reload(); - await expect(page.getByLabel('Name', {exact: true})).toHaveValue('NEW NAME'); + await expect(page.getByLabel('Name', { exact: true })).toHaveValue('NEW NAME'); }); -test.skip('test that user email can be updated', async ({page}) => { +test.skip('test that user email can be updated', async ({ page }) => { // this does not work because of email verification currently await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile'); const emailId = Math.round(Math.random() * 10000); await page.getByLabel('Email').fill(`newemail+${emailId}@test.com`); - await page.getByRole('button', {name: 'Save'}).first().click(); + await page.getByRole('button', { name: 'Save' }).first().click(); await page.reload(); - await expect(page.getByLabel('Email')).toHaveValue( - `newemail+${emailId}@test.com` - ); + await expect(page.getByLabel('Email')).toHaveValue(`newemail+${emailId}@test.com`); }); async function createNewApiToken(page) { await page.getByLabel('API Key Name').fill('NEW API KEY'); await Promise.all([ - page.getByRole('button', {name: 'Create API Key'}).click(), - page.waitForResponse('**/users/me/api-tokens') + page.getByRole('button', { name: 'Create API Key' }).click(), + page.waitForResponse('**/users/me/api-tokens'), ]); await expect(page.locator('body')).toContainText('API Token created successfully'); @@ -36,34 +34,37 @@ async function createNewApiToken(page) { await expect(page.locator('body')).toContainText('NEW API KEY'); } -test('test that user can create an API key', async ({page}) => { +test('test that user can create an API key', async ({ page }) => { await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile'); await createNewApiToken(page); }); -test('test that user can delete an API key', async ({page}) => { +test('test that user can delete an API key', async ({ page }) => { await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile'); await createNewApiToken(page); page.getByLabel('Delete API Token NEW API KEY').click(); - await expect(page.getByRole('dialog')).toContainText('Are you sure you would like to delete this API token?'); + await expect(page.getByRole('dialog')).toContainText( + 'Are you sure you would like to delete this API token?' + ); await Promise.all([ - page.getByRole('dialog').getByRole('button', {name: 'Delete'}).click(), - page.waitForResponse('**/users/me/api-tokens') + page.getByRole('dialog').getByRole('button', { name: 'Delete' }).click(), + page.waitForResponse('**/users/me/api-tokens'), ]); await expect(page.locator('body')).not.toContainText('NEW API KEY'); }); - -test('test that user can revoke an API key', async ({page}) => { +test('test that user can revoke an API key', async ({ page }) => { await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile'); await createNewApiToken(page); page.getByLabel('Revoke API Token NEW API KEY').click(); - await expect(page.getByRole('dialog')).toContainText('Are you sure you would like to revoke this API token?'); + await expect(page.getByRole('dialog')).toContainText( + 'Are you sure you would like to revoke this API token?' + ); await Promise.all([ - page.getByRole('dialog').getByRole('button', {name: 'Revoke'}).click(), - page.waitForResponse('**/users/me/api-tokens') + page.getByRole('dialog').getByRole('button', { name: 'Revoke' }).click(), + page.waitForResponse('**/users/me/api-tokens'), ]); - await expect(page.getByRole('button', {name: 'Revoke'})).toBeHidden(); + await expect(page.getByRole('button', { name: 'Revoke' })).toBeHidden(); await expect(page.locator('body')).toContainText('NEW API KEY'); await expect(page.locator('body')).toContainText('Revoked'); }); diff --git a/e2e/project-members.spec.ts b/e2e/project-members.spec.ts index 66434eab..36ee7f2b 100644 --- a/e2e/project-members.spec.ts +++ b/e2e/project-members.spec.ts @@ -12,8 +12,7 @@ async function goToProjectsOverview(page: Page) { test('test that updating project member billable rate works for existing time entries', async ({ page, }) => { - const newProjectName = - 'New Project ' + Math.floor(1 + Math.random() * 10000); + const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000); const newBillableRate = Math.round(Math.random() * 10000); await goToProjectsOverview(page); await page.getByRole('button', { name: 'Create Project' }).click(); @@ -36,9 +35,7 @@ test('test that updating project member billable rate works for existing time en .first() .getByRole('button') .click(); - await page - .getByRole('menuitem', { name: 'Edit Project Member' }) - .click(); + await page.getByRole('menuitem', { name: 'Edit Project Member' }).click(); await page.getByLabel('Billable Rate').fill(newBillableRate.toString()); await page.getByRole('button', { name: 'Update Project Member' }).click(); @@ -55,8 +52,7 @@ test('test that updating project member billable rate works for existing time en response.url().includes('/project-members/') && response.request().method() === 'PUT' && response.status() === 200 && - (await response.json()).data.billable_rate === - newBillableRate * 100 + (await response.json()).data.billable_rate === newBillableRate * 100 ), ]); await expect( diff --git a/e2e/projects.spec.ts b/e2e/projects.spec.ts index b751601a..2401ab1a 100644 --- a/e2e/projects.spec.ts +++ b/e2e/projects.spec.ts @@ -9,11 +9,8 @@ async function goToProjectsOverview(page: Page) { } // Create new project via modal -test('test that creating and deleting a new project via the modal works', async ({ - page, -}) => { - const newProjectName = - 'New Project ' + Math.floor(1 + Math.random() * 10000); +test.skip('test that creating and deleting a new project via the modal works', async ({ page }) => { + const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000); await goToProjectsOverview(page); await page.getByRole('button', { name: 'Create Project' }).click(); await page.getByLabel('Project Name').fill(newProjectName); @@ -31,16 +28,10 @@ test('test that creating and deleting a new project via the modal works', async ), ]); - await expect(page.getByTestId('project_table')).toContainText( - newProjectName - ); - const moreButton = page.locator( - "[aria-label='Actions for Project " + newProjectName + "']" - ); + await expect(page.getByTestId('project_table')).toContainText(newProjectName); + const moreButton = page.locator("[aria-label='Actions for Project " + newProjectName + "']"); moreButton.click(); - const deleteButton = page.locator( - "[aria-label='Delete Project " + newProjectName + "']" - ); + const deleteButton = page.locator("[aria-label='Delete Project " + newProjectName + "']"); await Promise.all([ deleteButton.click(), @@ -51,14 +42,11 @@ test('test that creating and deleting a new project via the modal works', async response.status() === 204 ), ]); - await expect(page.getByTestId('project_table')).not.toContainText( - newProjectName - ); + await expect(page.getByTestId('project_table')).not.toContainText(newProjectName); }); test('test that archiving and unarchiving projects works', async ({ page }) => { - const newProjectName = - 'New Project ' + Math.floor(1 + Math.random() * 10000); + const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000); await goToProjectsOverview(page); await page.getByRole('button', { name: 'Create Project' }).click(); await page.getByLabel('Project Name').fill(newProjectName); @@ -87,11 +75,8 @@ test('test that archiving and unarchiving projects works', async ({ page }) => { ]); }); -test('test that updating billable rate works with existing time entries', async ({ - page, -}) => { - const newProjectName = - 'New Project ' + Math.floor(1 + Math.random() * 10000); +test('test that updating billable rate works with existing time entries', async ({ page }) => { + const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000); const newBillableRate = Math.round(Math.random() * 10000); await goToProjectsOverview(page); await page.getByRole('button', { name: 'Create Project' }).click(); @@ -104,15 +89,11 @@ test('test that updating billable rate works with existing time entries', async await page.getByRole('menuitem').getByText('Edit').first().click(); await page.getByText('Non-Billable').click(); await page.getByText('Custom Rate').click(); - await page - .getByPlaceholder('Billable Rate') - .fill(newBillableRate.toString()); + await page.getByPlaceholder('Billable Rate').fill(newBillableRate.toString()); await page.getByRole('button', { name: 'Update Project' }).click(); await Promise.all([ - page - .locator('button').filter({ hasText: 'Yes, update existing time' }) - .click(), + page.locator('button').filter({ hasText: 'Yes, update existing time' }).click(), page.waitForRequest( async (request) => request.url().includes('/projects/') && @@ -124,8 +105,7 @@ test('test that updating billable rate works with existing time entries', async response.url().includes('/projects/') && response.request().method() === 'PUT' && response.status() === 200 && - (await response.json()).data.billable_rate === - newBillableRate * 100 + (await response.json()).data.billable_rate === newBillableRate * 100 ), ]); await expect( diff --git a/e2e/reporting.spec.ts b/e2e/reporting.spec.ts index 6b2f59c2..0cfa872c 100644 --- a/e2e/reporting.spec.ts +++ b/e2e/reporting.spec.ts @@ -2,8 +2,6 @@ import { expect, Page } from '@playwright/test'; import { PLAYWRIGHT_BASE_URL } from '../playwright/config'; import { test } from '../playwright/fixtures'; - - async function goToTimeOverview(page: Page) { await page.goto(PLAYWRIGHT_BASE_URL + '/time'); } @@ -31,7 +29,10 @@ async function createTimeEntryWithProject(page: Page, projectName: string, durat await page.getByRole('button', { name: 'Manual time entry' }).click(); // Fill in the time entry details - await page.getByRole('dialog').getByRole('textbox', { name: 'Description' }).fill(`Time entry for ${projectName}`); + await page + .getByRole('dialog') + .getByRole('textbox', { name: 'Description' }) + .fill(`Time entry for ${projectName}`); await page.getByRole('button', { name: 'No Project' }).click(); await page.getByText(projectName).click(); @@ -43,7 +44,9 @@ async function createTimeEntryWithProject(page: Page, projectName: string, durat // Submit the time entry await Promise.all([ page.getByRole('button', { name: 'Create Time Entry' }).click(), - page.waitForResponse(response => response.url().includes('/time-entries') && response.status() === 201) + page.waitForResponse( + (response) => response.url().includes('/time-entries') && response.status() === 201 + ), ]); } @@ -52,7 +55,10 @@ async function createTimeEntryWithTag(page: Page, tagName: string, duration: str await page.getByRole('button', { name: 'Manual time entry' }).click(); // Fill in the time entry details - await page.getByRole('dialog').getByRole('textbox', { name: 'Description' }).fill(`Time entry with tag ${tagName}`); + await page + .getByRole('dialog') + .getByRole('textbox', { name: 'Description' }) + .fill(`Time entry with tag ${tagName}`); // Add tag await page.getByRole('button', { name: 'Tags' }).click(); @@ -69,12 +75,19 @@ async function createTimeEntryWithTag(page: Page, tagName: string, duration: str await page.getByRole('button', { name: 'Create Time Entry' }).click(); } -async function createTimeEntryWithBillableStatus(page: Page, isBillable: boolean, duration: string) { +async function createTimeEntryWithBillableStatus( + page: Page, + isBillable: boolean, + duration: string +) { await goToTimeOverview(page); await page.getByRole('button', { name: 'Manual time entry' }).click(); // Fill in the time entry details - await page.getByRole('dialog').getByRole('textbox', { name: 'Description' }).fill(`Time entry ${isBillable ? 'billable' : 'non-billable'}`); + await page + .getByRole('dialog') + .getByRole('textbox', { name: 'Description' }) + .fill(`Time entry ${isBillable ? 'billable' : 'non-billable'}`); // Set billable status await page.getByRole('button', { name: 'Non-Billable' }).click(); @@ -109,7 +122,10 @@ test('test that project filtering works in reporting', async ({ page }) => { // escape page.keyboard.press('Escape'), // wait for API request to finish - page.waitForResponse(response => response.url().includes('/time-entries/aggregate') && response.status() === 200) + page.waitForResponse( + (response) => + response.url().includes('/time-entries/aggregate') && response.status() === 200 + ), ]); await page.waitForLoadState('networkidle'); @@ -138,7 +154,10 @@ test('test that tag filtering works in reporting', async ({ page }) => { // escape page.keyboard.press('Escape'), // wait for API request to finish - page.waitForResponse(response => response.url().includes('/time-entries/aggregate') && response.status() === 200) + page.waitForResponse( + (response) => + response.url().includes('/time-entries/aggregate') && response.status() === 200 + ), ]); // Verify only time entries with tag1 are shown @@ -160,14 +179,16 @@ test('test that billable status filtering works in reporting', async ({ page }) // escape page.keyboard.press('Escape'), // wait for API request to finish - page.waitForResponse(response => response.url().includes('/time-entries/aggregate') && response.status() === 200) + page.waitForResponse( + (response) => + response.url().includes('/time-entries/aggregate') && response.status() === 200 + ), ]); await page.waitForLoadState('networkidle'); await expect(page.getByTestId('reporting_view').getByText('1h 00min').first()).toBeVisible(); }); - test('test that detailed view shows time entries correctly', async ({ page }) => { const projectName = 'Detailed View Project ' + Math.floor(Math.random() * 10000); diff --git a/e2e/tags.spec.ts b/e2e/tags.spec.ts index 4a29c3db..8b89aa2b 100644 --- a/e2e/tags.spec.ts +++ b/e2e/tags.spec.ts @@ -7,9 +7,7 @@ async function goToTagsOverview(page: Page) { } // Create new project via modal -test('test that creating and deleting a new client via the modal works', async ({ - page, -}) => { +test('test that creating and deleting a new client via the modal works', async ({ page }) => { const newTagName = 'New Tag ' + Math.floor(1 + Math.random() * 10000); await goToTagsOverview(page); await page.getByRole('button', { name: 'Create Tag' }).click(); @@ -27,13 +25,9 @@ test('test that creating and deleting a new client via the modal works', async ( ]); await expect(page.getByTestId('tag_table')).toContainText(newTagName); - const moreButton = page.locator( - "[aria-label='Actions for Tag " + newTagName + "']" - ); + const moreButton = page.locator("[aria-label='Actions for Tag " + newTagName + "']"); moreButton.click(); - const deleteButton = page.locator( - "[aria-label='Delete Tag " + newTagName + "']" - ); + const deleteButton = page.locator("[aria-label='Delete Tag " + newTagName + "']"); await Promise.all([ deleteButton.click(), diff --git a/e2e/tasks.spec.ts b/e2e/tasks.spec.ts index d5165993..8e2073db 100644 --- a/e2e/tasks.spec.ts +++ b/e2e/tasks.spec.ts @@ -7,11 +7,8 @@ async function goToProjectsOverview(page: Page) { } // Create new project via modal -test('test that creating and deleting a new tag in a new project works', async ({ - page, -}) => { - const newProjectName = - 'New Project ' + Math.floor(1 + Math.random() * 10000); +test('test that creating and deleting a new tag in a new project works', async ({ page }) => { + const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000); await goToProjectsOverview(page); await page.getByRole('button', { name: 'Create Project' }).click(); await page.getByLabel('Project Name').fill(newProjectName); @@ -29,9 +26,7 @@ test('test that creating and deleting a new tag in a new project works', async ( ), ]); - await expect(page.getByTestId('project_table')).toContainText( - newProjectName - ); + await expect(page.getByTestId('project_table')).toContainText(newProjectName); await page.getByText(newProjectName).click(); @@ -55,13 +50,9 @@ test('test that creating and deleting a new tag in a new project works', async ( await expect(page.getByTestId('task_table')).toContainText(newTaskName); - const taskMoreButton = page.locator( - "[aria-label='Actions for Task " + newTaskName + "']" - ); + const taskMoreButton = page.locator("[aria-label='Actions for Task " + newTaskName + "']"); taskMoreButton.click(); - const taskDeleteButton = page.locator( - "[aria-label='Delete Task " + newTaskName + "']" - ); + const taskDeleteButton = page.locator("[aria-label='Delete Task " + newTaskName + "']"); await Promise.all([ taskDeleteButton.click(), @@ -76,13 +67,9 @@ test('test that creating and deleting a new tag in a new project works', async ( await goToProjectsOverview(page); - const moreButton = page.locator( - "[aria-label='Actions for Project " + newProjectName + "']" - ); + const moreButton = page.locator("[aria-label='Actions for Project " + newProjectName + "']"); moreButton.click(); - const deleteButton = page.locator( - "[aria-label='Delete Project " + newProjectName + "']" - ); + const deleteButton = page.locator("[aria-label='Delete Project " + newProjectName + "']"); await Promise.all([ deleteButton.click(), @@ -93,14 +80,11 @@ test('test that creating and deleting a new tag in a new project works', async ( response.status() === 204 ), ]); - await expect(page.getByTestId('project_table')).not.toContainText( - newProjectName - ); + await expect(page.getByTestId('project_table')).not.toContainText(newProjectName); }); test('test that archiving and unarchiving tasks works', async ({ page }) => { - const newProjectName = - 'New Project ' + Math.floor(1 + Math.random() * 10000); + const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000); const newTaskName = 'New Project ' + Math.floor(1 + Math.random() * 10000); await goToProjectsOverview(page); diff --git a/e2e/time.spec.ts b/e2e/time.spec.ts index da2aa7ce..2b01ddfb 100644 --- a/e2e/time.spec.ts +++ b/e2e/time.spec.ts @@ -25,9 +25,7 @@ async function createEmptyTimeEntry(page: Page) { startOrStopTimerWithButton(page), assertThatTimerIsStopped(page), page.waitForResponse( - (response) => - response.url().includes('/time-entries') && - response.status() === 200 + (response) => response.url().includes('/time-entries') && response.status() === 200 ), ]); } @@ -38,9 +36,7 @@ test('test that starting and stopping an empty time entry shows a new time entry await Promise.all([ goToTimeOverview(page), page.waitForResponse( - (response) => - response.url().includes('/time-entries') && - response.status() === 200 + (response) => response.url().includes('/time-entries') && response.status() === 200 ), ]); await page.waitForTimeout(100); @@ -56,9 +52,7 @@ test('test that starting and stopping an empty time entry shows a new time entry // Test that description update works async function assertThatTimeEntryRowIsStopped(newTimeEntry: Locator) { - await expect(newTimeEntry.getByTestId('timer_button')).toHaveClass( - /bg-accent-300\/70/ - ); + await expect(newTimeEntry.getByTestId('timer_button')).toHaveClass(/bg-accent-300\/70/); } test('test that updating a description of a time entry in the overview works on blur', async ({ @@ -71,17 +65,14 @@ test('test that updating a description of a time entry in the overview works on await assertThatTimeEntryRowIsStopped(newTimeEntry); const newDescription = Math.floor(Math.random() * 1000000).toString(); - const descriptionElement = newTimeEntry.getByTestId( - 'time_entry_description' - ); + const descriptionElement = newTimeEntry.getByTestId('time_entry_description'); await descriptionElement.fill(newDescription); await Promise.all([ descriptionElement.press('Tab'), page.waitForResponse(async (response) => { return ( response.status() === 200 && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.id !== null && (await response.json()).data.start !== null && (await response.json()).data.end !== null && @@ -90,8 +81,7 @@ test('test that updating a description of a time entry in the overview works on (await response.json()).data.task_id === null && (await response.json()).data.duration !== null && (await response.json()).data.user_id !== null && - JSON.stringify((await response.json()).data.tags) === - JSON.stringify([]) + JSON.stringify((await response.json()).data.tags) === JSON.stringify([]) ); }), ]); @@ -107,17 +97,14 @@ test('test that updating a description of a time entry in the overview works on const newTimeEntry = timeEntryRows.first(); await assertThatTimeEntryRowIsStopped(newTimeEntry); const newDescription = Math.floor(Math.random() * 1000000).toString(); - const descriptionElement = newTimeEntry.getByTestId( - 'time_entry_description' - ); + const descriptionElement = newTimeEntry.getByTestId('time_entry_description'); await descriptionElement.fill(newDescription); await Promise.all([ descriptionElement.press('Enter'), page.waitForResponse(async (response) => { return ( response.status() === 200 && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.id !== null && (await response.json()).data.start !== null && (await response.json()).data.end !== null && @@ -126,16 +113,13 @@ test('test that updating a description of a time entry in the overview works on (await response.json()).data.task_id === null && (await response.json()).data.duration !== null && (await response.json()).data.user_id !== null && - JSON.stringify((await response.json()).data.tags) === - JSON.stringify([]) + JSON.stringify((await response.json()).data.tags) === JSON.stringify([]) ); }), ]); }); -test('test that adding a new tag to an existing time entry works', async ({ - page, -}) => { +test('test that adding a new tag to an existing time entry works', async ({ page }) => { await goToTimeOverview(page); const timeEntryRows = page.locator('[data-testid="time_entry_row"]'); await createEmptyTimeEntry(page); @@ -152,8 +136,7 @@ test('test that adding a new tag to an existing time entry works', async ({ page.waitForResponse(async (response) => { return ( response.status() === 201 && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.name === newTagName ); }), @@ -163,8 +146,7 @@ test('test that adding a new tag to an existing time entry works', async ({ await page.waitForResponse(async (response) => { return ( response.status() === 200 && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.id !== null && (await response.json()).data.start !== null && (await response.json()).data.end !== null && @@ -187,17 +169,14 @@ test('test that updating a the start of an existing time entry in the overview w const newTimeEntry = timeEntryRows.first(); await assertThatTimeEntryRowIsStopped(newTimeEntry); await page.waitForTimeout(1500); - const timeEntryRangeElement = newTimeEntry.getByTestId( - 'time_entry_range_selector' - ); + const timeEntryRangeElement = newTimeEntry.getByTestId('time_entry_range_selector'); await timeEntryRangeElement.click(); await page.getByTestId('time_entry_range_start').first().fill('1'); await Promise.all([ page.waitForResponse(async (response) => { return ( response.status() === 200 && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.id !== null && // TODO! Actually check the value (await response.json()).data.start !== null && @@ -208,9 +187,7 @@ test('test that updating a the start of an existing time entry in the overview w ]); }); -test('test that updating a the duration in the overview works on blur', async ({ - page, -}) => { +test('test that updating a the duration in the overview works on blur', async ({ page }) => { await goToTimeOverview(page); const timeEntryRows = page.locator('[data-testid="time_entry_row"]'); await createEmptyTimeEntry(page); @@ -225,8 +202,7 @@ test('test that updating a the duration in the overview works on blur', async ({ page.waitForResponse(async (response) => { return ( response.status() === 200 && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.id !== null && // TODO! Actually check the value (await response.json()).data.start !== null && @@ -240,9 +216,7 @@ test('test that updating a the duration in the overview works on blur', async ({ }); // Test that start stop button stops running timer -test('test that starting a time entry from the overview works', async ({ - page, -}) => { +test('test that starting a time entry from the overview works', async ({ page }) => { await goToTimeOverview(page); const timeEntryRows = page.locator('[data-testid="time_entry_row"]'); await createEmptyTimeEntry(page); @@ -255,8 +229,7 @@ test('test that starting a time entry from the overview works', async ({ page.waitForResponse(async (response) => { return ( response.status() === 200 && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.id !== null && (await response.json()).data.start !== null && (await response.json()).data.end !== null @@ -272,8 +245,7 @@ test('test that starting a time entry from the overview works', async ({ page.waitForResponse(async (response) => { return ( response.status() === 200 && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.id !== null && (await response.json()).data.start !== null && (await response.json()).data.end !== null @@ -284,9 +256,7 @@ test('test that starting a time entry from the overview works', async ({ ]); }); -test('test that deleting a time entry from the overview works', async ({ - page, -}) => { +test('test that deleting a time entry from the overview works', async ({ page }) => { await goToTimeOverview(page); const timeEntryRows = page.locator('[data-testid="time_entry_row"]'); await createEmptyTimeEntry(page); @@ -302,16 +272,12 @@ test('test that deleting a time entry from the overview works', async ({ await expect(timeEntryRows).toHaveCount(0); }); -test.skip('test that load more works when the end of page is reached', async ({ - page, -}) => { +test.skip('test that load more works when the end of page is reached', async ({ page }) => { // this test is flaky when you do not need to scroll await Promise.all([ goToTimeOverview(page), page.waitForResponse( - (response) => - response.url().includes('/time-entries') && - response.status() === 200 + (response) => response.url().includes('/time-entries') && response.status() === 200 ), ]); @@ -322,18 +288,14 @@ test.skip('test that load more works when the end of page is reached', async ({ return ( response.status() === 200 && response.url().includes('before') && - (await response.headerValue('Content-Type')) === - 'application/json' && - JSON.stringify((await response.json()).data) === - JSON.stringify([]) + (await response.headerValue('Content-Type')) === 'application/json' && + JSON.stringify((await response.json()).data) === JSON.stringify([]) ); }), ]); // assert that "All time entries are loaded!" is visible on page - await expect(page.locator('body')).toHaveText( - /All time entries are loaded!/ - ); + await expect(page.locator('body')).toHaveText(/All time entries are loaded!/); }); // TODO: Test that updating the time entry start / end times works while it is running diff --git a/e2e/timetracker.spec.ts b/e2e/timetracker.spec.ts index df4a210f..5a371a3c 100644 --- a/e2e/timetracker.spec.ts +++ b/e2e/timetracker.spec.ts @@ -24,22 +24,15 @@ test('test that starting and stopping a timer without description and project wo assertThatTimerHasStarted(page), ]); await page.waitForTimeout(1500); - await Promise.all([ - stoppedTimeEntryResponse(page), - startOrStopTimerWithButton(page), - ]); + await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]); await assertThatTimerIsStopped(page); }); -test('test that starting and stopping a timer with a description works', async ({ - page, -}) => { +test('test that starting and stopping a timer with a description works', async ({ page }) => { await goToDashboard(page); // TODO: Fix flakyness by disabling description input field until timer is loaded await page.waitForTimeout(500); - await page - .getByTestId('time_entry_description') - .fill('New Time Entry Description'); + await page.getByTestId('time_entry_description').fill('New Time Entry Description'); await Promise.all([ newTimeEntryResponse(page, { description: 'New Time Entry Description', @@ -62,47 +55,29 @@ test('test that starting the time entry starts the live timer and that it keeps }) => { await goToDashboard(page); - await Promise.all([ - newTimeEntryResponse(page), - startOrStopTimerWithButton(page), - ]); + await Promise.all([newTimeEntryResponse(page), startOrStopTimerWithButton(page)]); await assertThatTimerHasStarted(page); await page.waitForTimeout(500); - const beforeTimerValue = await page - .getByTestId('time_entry_time') - .inputValue(); + const beforeTimerValue = await page.getByTestId('time_entry_time').inputValue(); await page.waitForTimeout(2000); - const afterWaitTimeValue = await page - .getByTestId('time_entry_time') - .inputValue(); + const afterWaitTimeValue = await page.getByTestId('time_entry_time').inputValue(); expect(afterWaitTimeValue).not.toEqual(beforeTimerValue); await page.reload(); await page.waitForTimeout(500); - const afterReloadTimerValue = await page - .getByTestId('time_entry_time') - .inputValue(); + const afterReloadTimerValue = await page.getByTestId('time_entry_time').inputValue(); await page.waitForTimeout(2000); - const afterReloadAfterWaitTimerValue = await page - .getByTestId('time_entry_time') - .inputValue(); + const afterReloadAfterWaitTimerValue = await page.getByTestId('time_entry_time').inputValue(); expect(afterReloadTimerValue).not.toEqual(afterReloadAfterWaitTimerValue); }); -test('test that starting and updating the description while running works', async ({ - page, -}) => { +test('test that starting and updating the description while running works', async ({ page }) => { await goToDashboard(page); - await Promise.all([ - newTimeEntryResponse(page), - startOrStopTimerWithButton(page), - ]); + await Promise.all([newTimeEntryResponse(page), startOrStopTimerWithButton(page)]); await assertThatTimerHasStarted(page); await page.waitForTimeout(500); - await page - .getByTestId('time_entry_description') - .fill('New Time Entry Description'); + await page.getByTestId('time_entry_description').fill('New Time Entry Description'); await Promise.all([ newTimeEntryResponse(page, { @@ -121,9 +96,7 @@ test('test that starting and updating the description while running works', asyn await assertThatTimerIsStopped(page); }); -test('test that starting and updating the time while running works', async ({ - page, -}) => { +test('test that starting and updating the time while running works', async ({ page }) => { await goToDashboard(page); const [createResponse] = await Promise.all([ newTimeEntryResponse(page), @@ -138,19 +111,16 @@ test('test that starting and updating the time while running works', async ({ return ( response.url().includes('/time-entries') && response.status() === 200 && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.id !== null && (await response.json()).data.start !== null && - (await response.json()).data.start !== - (await createResponse.json()).data.start && + (await response.json()).data.start !== (await createResponse.json()).data.start && (await response.json()).data.end === null && (await response.json()).data.project_id === null && (await response.json()).data.description === '' && (await response.json()).data.task_id === null && (await response.json()).data.user_id !== null && - JSON.stringify((await response.json()).data.tags) === - JSON.stringify([]) + JSON.stringify((await response.json()).data.tags) === JSON.stringify([]) ); }), page.getByTestId('time_entry_time').press('Enter'), @@ -158,16 +128,11 @@ test('test that starting and updating the time while running works', async ({ await expect(page.getByTestId('time_entry_time')).toHaveValue(/00:20/); await page.waitForTimeout(500); - await Promise.all([ - stoppedTimeEntryResponse(page), - startOrStopTimerWithButton(page), - ]); + await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]); await assertThatTimerIsStopped(page); }); -test('test that entering a human readable time starts the timer on blur', async ({ - page, -}) => { +test('test that entering a human readable time starts the timer on blur', async ({ page }) => { await goToDashboard(page); await page.getByTestId('time_entry_time').fill('20min'); await Promise.all([ @@ -177,18 +142,13 @@ test('test that entering a human readable time starts the timer on blur', async await expect(page.getByTestId('time_entry_time')).toHaveValue(/00:20:/); await assertThatTimerHasStarted(page); - await Promise.all([ - stoppedTimeEntryResponse(page), - startOrStopTimerWithButton(page), - ]); + await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]); await page.locator( '[data-testid="dashboard_timer"] [data-testid="timer_button"].bg-accent-300/70' ); }); -test('test that entering a number in the time range starts the timer on blur', async ({ - page, -}) => { +test('test that entering a number in the time range starts the timer on blur', async ({ page }) => { await goToDashboard(page); await page.getByTestId('time_entry_time').fill('5'); await Promise.all([ @@ -198,10 +158,7 @@ test('test that entering a number in the time range starts the timer on blur', a await expect(page.getByTestId('time_entry_time')).toHaveValue(/00:05:/); await assertThatTimerHasStarted(page); - await Promise.all([ - stoppedTimeEntryResponse(page), - startOrStopTimerWithButton(page), - ]); + await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]); await page.locator( '[data-testid="dashboard_timer"] [data-testid="timer_button"].bg-accent-300/70' ); @@ -219,10 +176,7 @@ test('test that entering a value with the format hh:mm in the time range starts await expect(page.getByTestId('time_entry_time')).toHaveValue(/12:30:/); await assertThatTimerHasStarted(page); - await Promise.all([ - stoppedTimeEntryResponse(page), - startOrStopTimerWithButton(page), - ]); + await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]); await page.locator( '[data-testid="dashboard_timer"] [data-testid="timer_button"].bg-accent-300/70' ); @@ -233,15 +187,13 @@ test('test that entering a random value in the time range does not start the tim }) => { await goToDashboard(page); await page.getByTestId('time_entry_time').fill('asdasdasd'); - await page.getByTestId('time_entry_time').press('Tab'), + (await page.getByTestId('time_entry_time').press('Tab'), await page.locator( '[data-testid="dashboard_timer"] [data-testid="timer_button"].bg-accent-300/70' - ); + )); }); -test('test that entering a time starts the timer on enter', async ({ - page, -}) => { +test('test that entering a time starts the timer on enter', async ({ page }) => { await goToDashboard(page); await page.getByTestId('time_entry_time').fill('20min'); await Promise.all([ @@ -249,10 +201,7 @@ test('test that entering a time starts the timer on enter', async ({ page.getByTestId('time_entry_time').press('Enter'), ]); await assertThatTimerHasStarted(page); - await Promise.all([ - stoppedTimeEntryResponse(page), - startOrStopTimerWithButton(page), - ]); + await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]); await assertThatTimerIsStopped(page); }); @@ -273,15 +222,10 @@ test('test that adding a new tag works', async ({ page }) => { await expect(page.getByRole('option', { name: newTagName })).toBeVisible(); }); -test('test that adding a new tag when the timer is running', async ({ - page, -}) => { +test('test that adding a new tag when the timer is running', async ({ page }) => { const newTagName = 'New Tag' + Math.floor(Math.random() * 10000); await goToDashboard(page); - await Promise.all([ - newTimeEntryResponse(page), - startOrStopTimerWithButton(page), - ]); + await Promise.all([newTimeEntryResponse(page), startOrStopTimerWithButton(page)]); await assertThatTimerHasStarted(page); await page.getByTestId('tag_dropdown').click(); await page.getByText('Create new tag').click(); diff --git a/e2e/utils/currentTimeEntry.ts b/e2e/utils/currentTimeEntry.ts index 720fddde..162975ec 100644 --- a/e2e/utils/currentTimeEntry.ts +++ b/e2e/utils/currentTimeEntry.ts @@ -1,9 +1,7 @@ import { expect, Page } from '@playwright/test'; export async function startOrStopTimerWithButton(page: Page) { - await page - .locator('[data-testid="dashboard_timer"] [data-testid="timer_button"]') - .click(); + await page.locator('[data-testid="dashboard_timer"] [data-testid="timer_button"]').click(); } export async function assertThatTimerHasStarted(page: Page) { @@ -20,8 +18,7 @@ export function newTimeEntryResponse( return ( response.url().includes('/time-entries') && response.status() === status && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.id !== null && (await response.json()).data.start !== null && (await response.json()).data.end === null && @@ -29,30 +26,23 @@ export function newTimeEntryResponse( (await response.json()).data.description === description && (await response.json()).data.task_id === null && (await response.json()).data.user_id !== null && - JSON.stringify((await response.json()).data.tags) === - JSON.stringify(tags) + JSON.stringify((await response.json()).data.tags) === JSON.stringify(tags) ); }); } export async function assertThatTimerIsStopped(page: Page) { await expect( - page.locator( - '[data-testid="dashboard_timer"] [data-testid="timer_button"]' - ) + page.locator('[data-testid="dashboard_timer"] [data-testid="timer_button"]') ).toHaveClass(/bg-accent-300\/70/); } -export async function stoppedTimeEntryResponse( - page: Page, - { description = '', tags = [] } = {} -) { +export async function stoppedTimeEntryResponse(page: Page, { description = '', tags = [] } = {}) { return page.waitForResponse(async (response) => { return ( response.status() === 200 && response.url().includes('/time-entries/') && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.id !== null && (await response.json()).data.start !== null && (await response.json()).data.end !== null && @@ -61,8 +51,7 @@ export async function stoppedTimeEntryResponse( (await response.json()).data.task_id === null && (await response.json()).data.duration !== null && (await response.json()).data.user_id !== null && - JSON.stringify((await response.json()).data.tags) === - JSON.stringify(tags) + JSON.stringify((await response.json()).data.tags) === JSON.stringify(tags) ); }); } diff --git a/e2e/utils/money.ts b/e2e/utils/money.ts index 34bf83dd..aed8176c 100644 --- a/e2e/utils/money.ts +++ b/e2e/utils/money.ts @@ -14,4 +14,4 @@ export function formatCentsWithOrganizationDefaults( currencySymbol, 'point-comma' as NumberFormat ); -} \ No newline at end of file +} diff --git a/e2e/utils/tags.ts b/e2e/utils/tags.ts index a0a3a701..888d6794 100644 --- a/e2e/utils/tags.ts +++ b/e2e/utils/tags.ts @@ -4,8 +4,7 @@ export function newTagResponse(page: Page, { name = '' } = {}) { return page.waitForResponse(async (response) => { return ( response.status() === 201 && - (await response.headerValue('Content-Type')) === - 'application/json' && + (await response.headerValue('Content-Type')) === 'application/json' && (await response.json()).data.name === name ); }); diff --git a/eslint.config.mjs b/eslint.config.mjs index 9b1c982f..2d6a685a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,7 +3,7 @@ import eslintConfigPrettier from 'eslint-config-prettier'; import eslintPluginVue from 'eslint-plugin-vue'; import globals from 'globals'; import typescriptEslint from 'typescript-eslint'; -import unusedImports from "eslint-plugin-unused-imports"; +import unusedImports from 'eslint-plugin-unused-imports'; export default typescriptEslint.config( { ignores: ['*.d.ts', '**/coverage', '**/dist'] }, @@ -23,18 +23,21 @@ export default typescriptEslint.config( }, }, plugins: { - "unused-imports": unusedImports, + 'unused-imports': unusedImports, }, rules: { - "vue/multi-word-component-names": "off", - "@typescript-eslint/no-unused-vars": "off", - "unused-imports/no-unused-imports": "error", - "unused-imports/no-unused-vars": ["error", { - "vars": "all", - "varsIgnorePattern": "^_", - "args": "after-used", - "argsIgnorePattern": "^_", - }], + 'vue/multi-word-component-names': 'off', + '@typescript-eslint/no-unused-vars': 'off', + 'unused-imports/no-unused-imports': 'error', + 'unused-imports/no-unused-vars': [ + 'error', + { + 'vars': 'all', + 'varsIgnorePattern': '^_', + 'args': 'after-used', + 'argsIgnorePattern': '^_', + }, + ], }, }, eslintConfigPrettier diff --git a/package-lock.json b/package-lock.json index 6d95ff79..d91270a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -335,13 +335,13 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", "dev": true, "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" + "@babel/types": "^7.28.2" }, "engines": { "node": ">=6.9.0" @@ -394,9 +394,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", - "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" @@ -904,9 +904,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", + "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -927,7 +927,6 @@ "version": "0.3.4", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", - "license": "Apache-2.0", "peer": true, "dependencies": { "@eslint/core": "^0.15.1", @@ -938,19 +937,19 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", - "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz", - "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.3.tgz", + "integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==", "dependencies": { - "@floating-ui/core": "^1.7.2", + "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, @@ -960,11 +959,11 @@ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" }, "node_modules/@floating-ui/vue": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.1.7.tgz", - "integrity": "sha512-idmAtbAIigGXN2SI5gItiXYBYtNfDTP9yIiObxgu13dgtG7ARCHlNfnR29GxP4LI4o13oiwsJ8wVgghj1lNqcw==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.1.8.tgz", + "integrity": "sha512-SNJAa1jbT8Gh1LvWw2uIIViLL0saV2bCY59ISCvJzhbut5DSb2H3LKUK49Xkd7SixTNHKX4LFu59nbwIXt9jjQ==", "dependencies": { - "@floating-ui/dom": "^1.7.2", + "@floating-ui/dom": "^1.7.3", "@floating-ui/utils": "^0.2.10", "vue-demi": ">=0.13.0" } @@ -1073,9 +1072,9 @@ } }, "node_modules/@internationalized/number": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.3.tgz", - "integrity": "sha512-p+Zh1sb6EfrfVaS86jlHGQ9HA66fJhV9x5LiE5vCbZtXEHAuhcmUZUdZ4WrFpUBfNalr2OkAJI5AcKEQF+Lebw==", + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.4.tgz", + "integrity": "sha512-P+/h+RDaiX8EGt3shB9AYM1+QgkvHmJ5rKi4/59k4sg9g58k9rqsRW0WxRO7jCoHyvVbFRRFKmVTdFYdehrxHg==", "dependencies": { "@swc/helpers": "^0.5.0" } @@ -1196,12 +1195,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz", - "integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==", + "version": "1.54.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.2.tgz", + "integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==", "dev": true, "dependencies": { - "playwright": "1.54.1" + "playwright": "1.54.2" }, "bin": { "playwright": "cli.js" @@ -1211,9 +1210,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz", - "integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", + "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", "cpu": [ "arm" ], @@ -1224,9 +1223,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.1.tgz", - "integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", + "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", "cpu": [ "arm64" ], @@ -1237,9 +1236,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.1.tgz", - "integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", + "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", "cpu": [ "arm64" ], @@ -1250,9 +1249,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.1.tgz", - "integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", + "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", "cpu": [ "x64" ], @@ -1263,9 +1262,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.1.tgz", - "integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", + "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", "cpu": [ "arm64" ], @@ -1276,9 +1275,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.1.tgz", - "integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", + "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", "cpu": [ "x64" ], @@ -1289,9 +1288,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.1.tgz", - "integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", + "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", "cpu": [ "arm" ], @@ -1302,9 +1301,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.1.tgz", - "integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", + "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", "cpu": [ "arm" ], @@ -1315,9 +1314,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.1.tgz", - "integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", + "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", "cpu": [ "arm64" ], @@ -1328,9 +1327,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.1.tgz", - "integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", + "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", "cpu": [ "arm64" ], @@ -1341,9 +1340,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.1.tgz", - "integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", + "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", "cpu": [ "loong64" ], @@ -1353,10 +1352,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.1.tgz", - "integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", + "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", "cpu": [ "ppc64" ], @@ -1367,9 +1366,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.1.tgz", - "integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", + "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", "cpu": [ "riscv64" ], @@ -1380,9 +1379,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.1.tgz", - "integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", + "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", "cpu": [ "riscv64" ], @@ -1393,9 +1392,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.1.tgz", - "integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", + "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", "cpu": [ "s390x" ], @@ -1406,9 +1405,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz", - "integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", "cpu": [ "x64" ], @@ -1419,9 +1418,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz", - "integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", + "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", "cpu": [ "x64" ], @@ -1432,9 +1431,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.1.tgz", - "integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", + "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", "cpu": [ "arm64" ], @@ -1445,9 +1444,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.1.tgz", - "integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", + "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", "cpu": [ "ia32" ], @@ -1458,9 +1457,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.1.tgz", - "integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", + "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", "cpu": [ "x64" ], @@ -1524,9 +1523,9 @@ } }, "node_modules/@tanstack/form-core": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-1.14.0.tgz", - "integrity": "sha512-uAOW3IxkT/Cmy8JlznK8S/LSpvtHjpUQi2wyuPqVfJ04y95WuV90SO+VKtb9TrNp51QLrrTFBR8tMEuzqp5wmA==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-1.17.0.tgz", + "integrity": "sha512-H2sFmlcpvZaNDNPBJpMXYhvEPwdlpVh57lqwHQyxW+qhzCIk1OJNV7TUTr0xY4jwpUHDET9OkoajbsKCyOCe+Q==", "dependencies": { "@tanstack/store": "^0.7.2" }, @@ -1551,18 +1550,18 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.83.0", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz", - "integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==", + "version": "5.83.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.1.tgz", + "integrity": "sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/query-devtools": { - "version": "5.81.2", - "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.81.2.tgz", - "integrity": "sha512-jCeJcDCwKfoyyBXjXe9+Lo8aTkavygHHsUHAlxQKKaDeyT0qyQNLKl7+UyqYH2dDF6UN/14873IPBHchcsU+Zg==", + "version": "5.84.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.84.0.tgz", + "integrity": "sha512-fbF3n+z1rqhvd9EoGp5knHkv3p5B2Zml1yNRjh7sNXklngYI5RVIWUrUjZ1RIcEoscarUb0+bOvIs5x9dwzOXQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -1599,11 +1598,11 @@ } }, "node_modules/@tanstack/vue-form": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@tanstack/vue-form/-/vue-form-1.14.0.tgz", - "integrity": "sha512-K+2m/E+WYPgr/m0wnhY6p++x8kdML0B8a2IXcjpDtJSFyAHL7LNO7Y0EH+r8C01JGAIWivZuVcb16U+EmlG0Cw==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@tanstack/vue-form/-/vue-form-1.17.0.tgz", + "integrity": "sha512-CflSjna09GPnpmC6Q6bG5ees18gk5I2FWLBsdzBrprXOU7H7EI6kbmFfk2yfmcuit/wkduKz9EwGLyDF2r/AUg==", "dependencies": { - "@tanstack/form-core": "1.14.0", + "@tanstack/form-core": "1.17.0", "@tanstack/vue-store": "^0.7.3" }, "funding": { @@ -1615,12 +1614,12 @@ } }, "node_modules/@tanstack/vue-query": { - "version": "5.83.0", - "resolved": "https://registry.npmjs.org/@tanstack/vue-query/-/vue-query-5.83.0.tgz", - "integrity": "sha512-sC3nnFEyAPOV4aGgt36ILrFIoR42UzRb+kqva96tiT3c80PCX99wDnVC5P01uxw6b3PaH7AD60HFFLBMFCKYow==", + "version": "5.83.1", + "resolved": "https://registry.npmjs.org/@tanstack/vue-query/-/vue-query-5.83.1.tgz", + "integrity": "sha512-Ss7LJGC2LZMZBsFFnrEfmQYcooDtJOjFZhmnXXUmlawk9EXHHDHUDaH2v7/r9jzyp2nOgABDQlDmjcBLXiyuvQ==", "dependencies": { "@tanstack/match-sorter-utils": "^8.19.4", - "@tanstack/query-core": "5.83.0", + "@tanstack/query-core": "5.83.1", "@vue/devtools-api": "^6.6.3", "vue-demi": "^0.14.10" }, @@ -1639,18 +1638,18 @@ } }, "node_modules/@tanstack/vue-query-devtools": { - "version": "5.83.0", - "resolved": "https://registry.npmjs.org/@tanstack/vue-query-devtools/-/vue-query-devtools-5.83.0.tgz", - "integrity": "sha512-HXQCfmC4ndeaMTv97tdprmN3dwT6z3LHxeJTpbl2R2qK+eFpctoqTyCvNhrRpOpOZabaaLN3rH2GCttHmHjU/Q==", + "version": "5.84.0", + "resolved": "https://registry.npmjs.org/@tanstack/vue-query-devtools/-/vue-query-devtools-5.84.0.tgz", + "integrity": "sha512-RA3bNvaIsFFngycBd7vF/hB4SwvSeTaZ7eiUDnWPbOwg8lxBZrBmF7d4uI3OYQwSmnA2B8tER2UWxkKgqrNFpg==", "dependencies": { - "@tanstack/query-devtools": "5.81.2" + "@tanstack/query-devtools": "5.84.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/vue-query": "^5.83.0", + "@tanstack/vue-query": "^5.83.1", "vue": "^3.3.0" } }, @@ -1729,9 +1728,9 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "node_modules/@types/node": { - "version": "22.16.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz", - "integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==", + "version": "22.17.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.0.tgz", + "integrity": "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==", "dev": true, "dependencies": { "undici-types": "~6.21.0" @@ -1743,15 +1742,15 @@ "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz", - "integrity": "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", + "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.37.0", - "@typescript-eslint/type-utils": "8.37.0", - "@typescript-eslint/utils": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/type-utils": "8.39.0", + "@typescript-eslint/utils": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1765,9 +1764,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.37.0", + "@typescript-eslint/parser": "^8.39.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -1779,14 +1778,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz", - "integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==", - "dependencies": { - "@typescript-eslint/scope-manager": "8.37.0", - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/typescript-estree": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz", + "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", + "dependencies": { + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4" }, "engines": { @@ -1798,16 +1797,16 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz", - "integrity": "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", + "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.37.0", - "@typescript-eslint/types": "^8.37.0", + "@typescript-eslint/tsconfig-utils": "^8.39.0", + "@typescript-eslint/types": "^8.39.0", "debug": "^4.3.4" }, "engines": { @@ -1818,16 +1817,16 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz", - "integrity": "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", + "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", "dependencies": { - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0" + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1838,9 +1837,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz", - "integrity": "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", + "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1849,17 +1848,17 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz", - "integrity": "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", + "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", "dependencies": { - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/typescript-estree": "8.37.0", - "@typescript-eslint/utils": "8.37.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/utils": "8.39.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1872,13 +1871,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.37.0.tgz", - "integrity": "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz", + "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1888,14 +1887,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz", - "integrity": "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==", - "dependencies": { - "@typescript-eslint/project-service": "8.37.0", - "@typescript-eslint/tsconfig-utils": "8.37.0", - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", + "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", + "dependencies": { + "@typescript-eslint/project-service": "8.39.0", + "@typescript-eslint/tsconfig-utils": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1911,7 +1910,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -1937,14 +1936,14 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.37.0.tgz", - "integrity": "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz", + "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.37.0", - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/typescript-estree": "8.37.0" + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1955,15 +1954,15 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz", - "integrity": "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", + "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", "dependencies": { - "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/types": "8.39.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -2025,36 +2024,36 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.17.tgz", - "integrity": "sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA==", + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.18.tgz", + "integrity": "sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==", "dependencies": { - "@babel/parser": "^7.27.5", - "@vue/shared": "3.5.17", + "@babel/parser": "^7.28.0", + "@vue/shared": "3.5.18", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.17.tgz", - "integrity": "sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ==", + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.18.tgz", + "integrity": "sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A==", "dependencies": { - "@vue/compiler-core": "3.5.17", - "@vue/shared": "3.5.17" + "@vue/compiler-core": "3.5.18", + "@vue/shared": "3.5.18" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.17.tgz", - "integrity": "sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww==", - "dependencies": { - "@babel/parser": "^7.27.5", - "@vue/compiler-core": "3.5.17", - "@vue/compiler-dom": "3.5.17", - "@vue/compiler-ssr": "3.5.17", - "@vue/shared": "3.5.17", + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.18.tgz", + "integrity": "sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==", + "dependencies": { + "@babel/parser": "^7.28.0", + "@vue/compiler-core": "3.5.18", + "@vue/compiler-dom": "3.5.18", + "@vue/compiler-ssr": "3.5.18", + "@vue/shared": "3.5.18", "estree-walker": "^2.0.2", "magic-string": "^0.30.17", "postcss": "^8.5.6", @@ -2062,12 +2061,12 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.17.tgz", - "integrity": "sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ==", + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.18.tgz", + "integrity": "sha512-xM16Ak7rSWHkM3m22NlmcdIM+K4BMyFARAfV9hYFl+SFuRzrZ3uGMNW05kA5pmeMa0X9X963Kgou7ufdbpOP9g==", "dependencies": { - "@vue/compiler-dom": "3.5.17", - "@vue/shared": "3.5.17" + "@vue/compiler-dom": "3.5.18", + "@vue/shared": "3.5.18" } }, "node_modules/@vue/compiler-vue2": { @@ -2171,49 +2170,49 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.17.tgz", - "integrity": "sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw==", + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.18.tgz", + "integrity": "sha512-x0vPO5Imw+3sChLM5Y+B6G1zPjwdOri9e8V21NnTnlEvkxatHEH5B5KEAJcjuzQ7BsjGrKtfzuQ5eQwXh8HXBg==", "dependencies": { - "@vue/shared": "3.5.17" + "@vue/shared": "3.5.18" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.17.tgz", - "integrity": "sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q==", + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.18.tgz", + "integrity": "sha512-DUpHa1HpeOQEt6+3nheUfqVXRog2kivkXHUhoqJiKR33SO4x+a5uNOMkV487WPerQkL0vUuRvq/7JhRgLW3S+w==", "dependencies": { - "@vue/reactivity": "3.5.17", - "@vue/shared": "3.5.17" + "@vue/reactivity": "3.5.18", + "@vue/shared": "3.5.18" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.17.tgz", - "integrity": "sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g==", + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.18.tgz", + "integrity": "sha512-YwDj71iV05j4RnzZnZtGaXwPoUWeRsqinblgVJwR8XTXYZ9D5PbahHQgsbmzUvCWNF6x7siQ89HgnX5eWkr3mw==", "dependencies": { - "@vue/reactivity": "3.5.17", - "@vue/runtime-core": "3.5.17", - "@vue/shared": "3.5.17", + "@vue/reactivity": "3.5.18", + "@vue/runtime-core": "3.5.18", + "@vue/shared": "3.5.18", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.17.tgz", - "integrity": "sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA==", + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.18.tgz", + "integrity": "sha512-PvIHLUoWgSbDG7zLHqSqaCoZvHi6NNmfVFOqO+OnwvqMz/tqQr3FuGWS8ufluNddk7ZLBJYMrjcw1c6XzR12mA==", "dependencies": { - "@vue/compiler-ssr": "3.5.17", - "@vue/shared": "3.5.17" + "@vue/compiler-ssr": "3.5.18", + "@vue/shared": "3.5.18" }, "peerDependencies": { - "vue": "3.5.17" + "vue": "3.5.18" } }, "node_modules/@vue/shared": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.17.tgz", - "integrity": "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==" + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.18.tgz", + "integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==" }, "node_modules/@vue/tsconfig": { "version": "0.5.1", @@ -2507,7 +2506,6 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "devOptional": true, - "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -2649,9 +2647,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", "dev": true, "funding": [ { @@ -2907,9 +2905,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.187", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz", - "integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==", + "version": "1.5.195", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.195.tgz", + "integrity": "sha512-URclP0iIaDUzqcAyV1v2PgduJ9N0IdXmWsnPzPfelvBmjmZzEy6xJcjb1cXj+TbYqXgtLrjHEoaSIdTYhw4ezg==", "dev": true }, "node_modules/emoji-regex": { @@ -3036,9 +3034,9 @@ } }, "node_modules/eslint": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", - "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", + "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -3047,8 +3045,8 @@ "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.31.0", - "@eslint/plugin-kit": "^0.3.1", + "@eslint/js": "9.32.0", + "@eslint/plugin-kit": "^0.3.4", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -3154,9 +3152,9 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.3.0.tgz", - "integrity": "sha512-A0u9snqjCfYaPnqqOaH6MBLVWDUIN4trXn8J3x67uDcXvR7X6Ut8p16N+nYhMCQ9Y7edg2BIRGzfyZsY0IdqoQ==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.4.0.tgz", + "integrity": "sha512-K6tP0dW8FJVZLQxa2S7LcE1lLw3X8VvB3t887Q6CLrFVxHYBXGANbXvwNzYIu6Ughx1bSJ5BDT0YB3ybPT39lw==", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", @@ -3446,9 +3444,9 @@ } }, "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==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "devOptional": true, "funding": [ { @@ -4541,12 +4539,12 @@ } }, "node_modules/playwright": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz", - "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", + "version": "1.54.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz", + "integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==", "dev": true, "dependencies": { - "playwright-core": "1.54.1" + "playwright-core": "1.54.2" }, "bin": { "playwright": "cli.js" @@ -4559,9 +4557,9 @@ } }, "node_modules/playwright-core": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz", - "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", + "version": "1.54.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz", + "integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -4982,9 +4980,9 @@ } }, "node_modules/reka-ui": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.3.2.tgz", - "integrity": "sha512-lCysSCILH2uqShEnt93/qzlXnB7ySvK7scR0Q5C+a2iXwFVzHhvZQsMaSnbQYueoCihx6yyUZTYECepnmKrbRA==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.4.1.tgz", + "integrity": "sha512-NB7DrCsODN8MH02BWtgiExygfFcuuZ5/PTn6fMgjppmFHqePvNhmSn1LEuF35nel6PFbA4v+gdj0IoGN1yZ+vw==", "dependencies": { "@floating-ui/dom": "^1.6.13", "@floating-ui/vue": "^1.1.6", @@ -5052,9 +5050,9 @@ } }, "node_modules/rollup": { - "version": "4.45.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", - "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", + "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", "dev": true, "dependencies": { "@types/estree": "1.0.8" @@ -5067,26 +5065,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.45.1", - "@rollup/rollup-android-arm64": "4.45.1", - "@rollup/rollup-darwin-arm64": "4.45.1", - "@rollup/rollup-darwin-x64": "4.45.1", - "@rollup/rollup-freebsd-arm64": "4.45.1", - "@rollup/rollup-freebsd-x64": "4.45.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", - "@rollup/rollup-linux-arm-musleabihf": "4.45.1", - "@rollup/rollup-linux-arm64-gnu": "4.45.1", - "@rollup/rollup-linux-arm64-musl": "4.45.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", - "@rollup/rollup-linux-riscv64-gnu": "4.45.1", - "@rollup/rollup-linux-riscv64-musl": "4.45.1", - "@rollup/rollup-linux-s390x-gnu": "4.45.1", - "@rollup/rollup-linux-x64-gnu": "4.45.1", - "@rollup/rollup-linux-x64-musl": "4.45.1", - "@rollup/rollup-win32-arm64-msvc": "4.45.1", - "@rollup/rollup-win32-ia32-msvc": "4.45.1", - "@rollup/rollup-win32-x64-msvc": "4.45.1", + "@rollup/rollup-android-arm-eabi": "4.46.2", + "@rollup/rollup-android-arm64": "4.46.2", + "@rollup/rollup-darwin-arm64": "4.46.2", + "@rollup/rollup-darwin-x64": "4.46.2", + "@rollup/rollup-freebsd-arm64": "4.46.2", + "@rollup/rollup-freebsd-x64": "4.46.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", + "@rollup/rollup-linux-arm-musleabihf": "4.46.2", + "@rollup/rollup-linux-arm64-gnu": "4.46.2", + "@rollup/rollup-linux-arm64-musl": "4.46.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", + "@rollup/rollup-linux-ppc64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-musl": "4.46.2", + "@rollup/rollup-linux-s390x-gnu": "4.46.2", + "@rollup/rollup-linux-x64-gnu": "4.46.2", + "@rollup/rollup-linux-x64-musl": "4.46.2", + "@rollup/rollup-win32-arm64-msvc": "4.46.2", + "@rollup/rollup-win32-ia32-msvc": "4.46.2", + "@rollup/rollup-win32-x64-msvc": "4.46.2", "fsevents": "~2.3.2" } }, @@ -5592,9 +5590,9 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, "node_modules/ts-pattern": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.7.1.tgz", - "integrity": "sha512-EGs8PguQqAAUIcQfK4E9xdXxB6s2GK4sJfT/vcc9V1ELIvC4LH/zXu2t/5fajtv6oiRCxdv7BgtVK3vWgROxag==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.8.0.tgz", + "integrity": "sha512-kIjN2qmWiHnhgr5DAkAafF9fwb0T5OhMVSWrm8XEdTFnX6+wfXwYOFjeF86UZ54vduqiR7BfqScFmXSzSaH8oA==", "dev": true }, "node_modules/ts-toolbelt": { @@ -5633,9 +5631,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5645,14 +5643,14 @@ } }, "node_modules/typescript-eslint": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.37.0.tgz", - "integrity": "sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.39.0.tgz", + "integrity": "sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q==", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.37.0", - "@typescript-eslint/parser": "8.37.0", - "@typescript-eslint/typescript-estree": "8.37.0", - "@typescript-eslint/utils": "8.37.0" + "@typescript-eslint/eslint-plugin": "8.39.0", + "@typescript-eslint/parser": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/utils": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5663,7 +5661,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/uglify-js": { @@ -6035,15 +6033,15 @@ "dev": true }, "node_modules/vue": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.17.tgz", - "integrity": "sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g==", - "dependencies": { - "@vue/compiler-dom": "3.5.17", - "@vue/compiler-sfc": "3.5.17", - "@vue/runtime-dom": "3.5.17", - "@vue/server-renderer": "3.5.17", - "@vue/shared": "3.5.17" + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.18.tgz", + "integrity": "sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==", + "dependencies": { + "@vue/compiler-dom": "3.5.18", + "@vue/compiler-sfc": "3.5.18", + "@vue/runtime-dom": "3.5.18", + "@vue/server-renderer": "3.5.18", + "@vue/shared": "3.5.18" }, "peerDependencies": { "typescript": "*" diff --git a/package.json b/package.json index 389fece5..8770220b 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "lint:fix": "eslint --fix resources/js", "type-check": "vue-tsc --noEmit", "test:e2e": "rm -rf test-results/.auth && npx playwright test", - "zod:generate": "npx openapi-zod-client http://localhost:80/docs/api.json --output resources/js/packages/api/src/openapi.json.client.ts --base-url /api" + "zod:generate": "npx openapi-zod-client http://localhost:80/docs/api.json --output resources/js/packages/api/src/openapi.json.client.ts --base-url /api", + "format": "prettier --write './**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,vue}'", + "format:check": "prettier --check './**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,vue}'" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", diff --git a/playwright/config.ts b/playwright/config.ts index 7c6db05e..e44637f3 100644 --- a/playwright/config.ts +++ b/playwright/config.ts @@ -1,2 +1 @@ -export const PLAYWRIGHT_BASE_URL = - process.env.PLAYWRIGHT_BASE_URL ?? 'https://time.codebar.test'; +export const PLAYWRIGHT_BASE_URL = process.env.PLAYWRIGHT_BASE_URL ?? 'https://time.codebar.test'; diff --git a/playwright/fixtures.ts b/playwright/fixtures.ts index 80fadd92..93469ad3 100644 --- a/playwright/fixtures.ts +++ b/playwright/fixtures.ts @@ -8,12 +8,8 @@ export const test = baseTest.extend({ // Perform authentication steps. Replace these actions with your own. await page.goto(PLAYWRIGHT_BASE_URL + '/register'); await page.getByLabel('Name').fill('John Doe'); - await page - .getByLabel('Email') - .fill(`john+${Math.round(Math.random() * 1000000)}@doe.com`); - await page - .getByLabel('Password', { exact: true }) - .fill('amazingpassword123'); + await page.getByLabel('Email').fill(`john+${Math.round(Math.random() * 1000000)}@doe.com`); + await page.getByLabel('Password', { exact: true }).fill('amazingpassword123'); await page.getByLabel('Confirm Password').fill('amazingpassword123'); await page.getByLabel('I agree to the Terms of').click(); await page.getByRole('button', { name: 'Register' }).click(); diff --git a/public/fonts/Inter-Variable.ttf b/public/fonts/Inter-Variable.ttf deleted file mode 100644 index b0f2d424..00000000 --- a/public/fonts/Inter-Variable.ttf +++ /dev/null @@ -1,2052 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Page not found · GitHub · GitHub - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- Skip to content - - - - - - - - - - - - - - - - - - - - -
-
- - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - -
- - - - - - - - - -
-
- - - -
-
- -
-
- 404 “This is not the web page you are looking for” - - - - - - - - - - - - -
-
- -
-
- -
- - -
-
- -
- -
- -
- - - - - - - - - - - - - - - - - - - - - -
-
-
- - - diff --git a/public/fonts/Inter-Variable.woff2 b/public/fonts/Inter-Variable.woff2 deleted file mode 100644 index dc8ac65f..00000000 --- a/public/fonts/Inter-Variable.woff2 +++ /dev/null @@ -1,2052 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Page not found · GitHub · GitHub - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- Skip to content - - - - - - - - - - - - - - - - - - - - -
-
- - - - - - - - - - - - - - - -
- -
- - - - - - - - -
- - - - - -
- - - - - - - - - -
-
- - - -
-
- -
-
- 404 “This is not the web page you are looking for” - - - - - - - - - - - - -
-
- -
-
- -
- - -
-
- -
- -
- -
- - - - - - - - - - - - - - - - - - - - - -
-
-
- - - diff --git a/public/fonts/InterVariable.ttf b/public/fonts/InterVariable.ttf new file mode 100644 index 00000000..4ab79e01 Binary files /dev/null and b/public/fonts/InterVariable.ttf differ diff --git a/public/fonts/InterVariable.woff2 b/public/fonts/InterVariable.woff2 new file mode 100644 index 00000000..5a8d3e72 Binary files /dev/null and b/public/fonts/InterVariable.woff2 differ diff --git a/resources/css/app.css b/resources/css/app.css index b5cf17cd..5e6b531a 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -163,10 +163,8 @@ body { /* Inter Variable Font with browser compatibility considerations */ @font-face { font-family: 'Inter'; - src: url('/fonts/Inter-Variable.woff2') format('woff2 supports variations'), - url('/fonts/Inter-Variable.woff2') format('woff2-variations'), - url('/fonts/Inter-Variable.ttf') format('truetype supports variations'), - url('/fonts/Inter-Variable.ttf') format('truetype-variations'); + src: url('/fonts/InterVariable.woff2') format('woff2'), + url('/fonts/InterVariable.ttf') format('truetype'); font-weight: 100 900; font-style: normal; font-display: swap; diff --git a/resources/js/Components/ActionSection.vue b/resources/js/Components/ActionSection.vue index 6d07781e..6748475c 100644 --- a/resources/js/Components/ActionSection.vue +++ b/resources/js/Components/ActionSection.vue @@ -14,8 +14,7 @@ import SectionTitle from './SectionTitle.vue';
-
+
diff --git a/resources/js/Components/AuthenticationCard.vue b/resources/js/Components/AuthenticationCard.vue index 30ffea9b..4a8c6c17 100644 --- a/resources/js/Components/AuthenticationCard.vue +++ b/resources/js/Components/AuthenticationCard.vue @@ -1,9 +1,9 @@ diff --git a/resources/js/Components/Banner.vue b/resources/js/Components/Banner.vue index 4c69e0ff..475f0eae 100644 --- a/resources/js/Components/Banner.vue +++ b/resources/js/Components/Banner.vue @@ -24,9 +24,7 @@ watchEffect(async () => { \ No newline at end of file + diff --git a/resources/js/Components/Common/Client/ClientTableHeading.vue b/resources/js/Components/Common/Client/ClientTableHeading.vue index 15a9dd68..7e24e3b6 100644 --- a/resources/js/Components/Common/Client/ClientTableHeading.vue +++ b/resources/js/Components/Common/Client/ClientTableHeading.vue @@ -4,7 +4,9 @@ import TableHeading from '@/Components/Common/TableHeading.vue';