From 8b9f454c038768f67bd139c6db63252dd9c67d37 Mon Sep 17 00:00:00 2001 From: Cinzya Date: Sun, 28 Sep 2025 20:04:39 +0200 Subject: [PATCH 01/28] fix(ui): update docker registry image helper text for clarity --- resources/views/livewire/project/application/general.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index ba7d2edb0..0fbd80bce 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -187,7 +187,7 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" x-bind:disabled="!canUpdate" /> @else Date: Mon, 29 Sep 2025 13:02:29 +0200 Subject: [PATCH 02/28] refactor(global-search): change event listener to window level for global search modal --- resources/views/livewire/global-search.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/livewire/global-search.blade.php b/resources/views/livewire/global-search.blade.php index 1595fa486..0b9b61da4 100644 --- a/resources/views/livewire/global-search.blade.php +++ b/resources/views/livewire/global-search.blade.php @@ -31,8 +31,8 @@ } }, init() { - // Listen for custom event from navbar search button - this.$el.addEventListener('open-global-search', () => { + // Listen for custom event from navbar search button at window level + window.addEventListener('open-global-search', () => { this.openModal(); }); From 72f5ae0dc6cff88d54ee86d093254e297a432889 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 29 Sep 2025 14:03:49 +0200 Subject: [PATCH 03/28] feat(user-deletion): implement file locking to prevent concurrent user deletions and enhance error handling --- .../Commands/Cloud/CloudDeleteUser.php | 210 ++++++++++-------- 1 file changed, 116 insertions(+), 94 deletions(-) diff --git a/app/Console/Commands/Cloud/CloudDeleteUser.php b/app/Console/Commands/Cloud/CloudDeleteUser.php index 29580a95e..a2ea9b3e5 100644 --- a/app/Console/Commands/Cloud/CloudDeleteUser.php +++ b/app/Console/Commands/Cloud/CloudDeleteUser.php @@ -8,6 +8,7 @@ use App\Actions\User\DeleteUserTeams; use App\Models\User; use Illuminate\Console\Command; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; @@ -54,124 +55,141 @@ public function handle() return 1; } - $this->logAction("Starting user deletion process for: {$email}"); + // Implement file lock to prevent concurrent deletions of the same user + $lockKey = "user_deletion_{$this->user->id}"; + $lock = Cache::lock($lockKey, 600); // 10 minute lock - // Phase 1: Show User Overview (outside transaction) - if (! $this->showUserOverview()) { - $this->info('User deletion cancelled.'); + if (! $lock->get()) { + $this->error('Another deletion process is already running for this user. Please try again later.'); + $this->logAction("Deletion blocked for user {$email}: Another process is already running"); - return 0; + return 1; } - // If not dry run, wrap everything in a transaction - if (! $this->isDryRun) { - try { - DB::beginTransaction(); + try { + $this->logAction("Starting user deletion process for: {$email}"); + // Phase 1: Show User Overview (outside transaction) + if (! $this->showUserOverview()) { + $this->info('User deletion cancelled.'); + $lock->release(); + + return 0; + } + + // If not dry run, wrap everything in a transaction + if (! $this->isDryRun) { + try { + DB::beginTransaction(); + + // Phase 2: Delete Resources + if (! $this->skipResources) { + if (! $this->deleteResources()) { + DB::rollBack(); + $this->error('User deletion failed at resource deletion phase. All changes rolled back.'); + + return 1; + } + } + + // Phase 3: Delete Servers + if (! $this->deleteServers()) { + DB::rollBack(); + $this->error('User deletion failed at server deletion phase. All changes rolled back.'); + + return 1; + } + + // Phase 4: Handle Teams + if (! $this->handleTeams()) { + DB::rollBack(); + $this->error('User deletion failed at team handling phase. All changes rolled back.'); + + return 1; + } + + // Phase 5: Cancel Stripe Subscriptions + if (! $this->skipStripe && isCloud()) { + if (! $this->cancelStripeSubscriptions()) { + DB::rollBack(); + $this->error('User deletion failed at Stripe cancellation phase. All changes rolled back.'); + + return 1; + } + } + + // Phase 6: Delete User Profile + if (! $this->deleteUserProfile()) { + DB::rollBack(); + $this->error('User deletion failed at final phase. All changes rolled back.'); + + return 1; + } + + // Commit the transaction + DB::commit(); + + $this->newLine(); + $this->info('✅ User deletion completed successfully!'); + $this->logAction("User deletion completed for: {$email}"); + + } catch (\Exception $e) { + DB::rollBack(); + $this->error('An error occurred during user deletion: '.$e->getMessage()); + $this->logAction("User deletion failed for {$email}: ".$e->getMessage()); + + return 1; + } + } else { + // Dry run mode - just run through the phases without transaction // Phase 2: Delete Resources if (! $this->skipResources) { if (! $this->deleteResources()) { - DB::rollBack(); - $this->error('User deletion failed at resource deletion phase. All changes rolled back.'); + $this->info('User deletion would be cancelled at resource deletion phase.'); - return 1; + return 0; } } // Phase 3: Delete Servers if (! $this->deleteServers()) { - DB::rollBack(); - $this->error('User deletion failed at server deletion phase. All changes rolled back.'); + $this->info('User deletion would be cancelled at server deletion phase.'); - return 1; + return 0; } // Phase 4: Handle Teams if (! $this->handleTeams()) { - DB::rollBack(); - $this->error('User deletion failed at team handling phase. All changes rolled back.'); + $this->info('User deletion would be cancelled at team handling phase.'); - return 1; + return 0; } // Phase 5: Cancel Stripe Subscriptions if (! $this->skipStripe && isCloud()) { if (! $this->cancelStripeSubscriptions()) { - DB::rollBack(); - $this->error('User deletion failed at Stripe cancellation phase. All changes rolled back.'); + $this->info('User deletion would be cancelled at Stripe cancellation phase.'); - return 1; + return 0; } } // Phase 6: Delete User Profile if (! $this->deleteUserProfile()) { - DB::rollBack(); - $this->error('User deletion failed at final phase. All changes rolled back.'); + $this->info('User deletion would be cancelled at final phase.'); - return 1; + return 0; } - // Commit the transaction - DB::commit(); - $this->newLine(); - $this->info('✅ User deletion completed successfully!'); - $this->logAction("User deletion completed for: {$email}"); - - } catch (\Exception $e) { - DB::rollBack(); - $this->error('An error occurred during user deletion: '.$e->getMessage()); - $this->logAction("User deletion failed for {$email}: ".$e->getMessage()); - - return 1; - } - } else { - // Dry run mode - just run through the phases without transaction - // Phase 2: Delete Resources - if (! $this->skipResources) { - if (! $this->deleteResources()) { - $this->info('User deletion would be cancelled at resource deletion phase.'); - - return 0; - } + $this->info('✅ DRY RUN completed successfully! No data was deleted.'); } - // Phase 3: Delete Servers - if (! $this->deleteServers()) { - $this->info('User deletion would be cancelled at server deletion phase.'); - - return 0; - } - - // Phase 4: Handle Teams - if (! $this->handleTeams()) { - $this->info('User deletion would be cancelled at team handling phase.'); - - return 0; - } - - // Phase 5: Cancel Stripe Subscriptions - if (! $this->skipStripe && isCloud()) { - if (! $this->cancelStripeSubscriptions()) { - $this->info('User deletion would be cancelled at Stripe cancellation phase.'); - - return 0; - } - } - - // Phase 6: Delete User Profile - if (! $this->deleteUserProfile()) { - $this->info('User deletion would be cancelled at final phase.'); - - return 0; - } - - $this->newLine(); - $this->info('✅ DRY RUN completed successfully! No data was deleted.'); + return 0; + } finally { + // Ensure lock is always released + $lock->release(); } - - return 0; } private function showUserOverview(): bool @@ -683,24 +701,21 @@ private function deleteUserProfile(): bool private function getSubscriptionMonthlyValue(string $planId): int { - // Map plan IDs to monthly values based on config - $subscriptionConfigs = config('subscription'); + // Try to get pricing from subscription metadata or config + // Since we're using dynamic pricing, return 0 for now + // This could be enhanced by fetching the actual price from Stripe API - foreach ($subscriptionConfigs as $key => $value) { - if ($value === $planId && str_contains($key, 'stripe_price_id_')) { - // Extract price from key pattern: stripe_price_id_basic_monthly -> basic - $planType = str($key)->after('stripe_price_id_')->before('_')->toString(); + // Check if this is a dynamic pricing plan + $dynamicMonthlyPlanId = config('subscription.stripe_price_id_dynamic_monthly'); + $dynamicYearlyPlanId = config('subscription.stripe_price_id_dynamic_yearly'); - // Map to known prices (you may need to adjust these based on your actual pricing) - return match ($planType) { - 'basic' => 29, - 'pro' => 49, - 'ultimate' => 99, - default => 0 - }; - } + if ($planId === $dynamicMonthlyPlanId || $planId === $dynamicYearlyPlanId) { + // For dynamic pricing, we can't determine the exact amount without calling Stripe API + // Return 0 to indicate dynamic/usage-based pricing + return 0; } + // For any other plans, return 0 as we don't have hardcoded prices return 0; } @@ -716,6 +731,13 @@ private function logAction(string $message): void // Also log to a dedicated user deletion log file $logFile = storage_path('logs/user-deletions.log'); + + // Ensure the logs directory exists + $logDir = dirname($logFile); + if (! is_dir($logDir)) { + mkdir($logDir, 0755, true); + } + $timestamp = now()->format('Y-m-d H:i:s'); file_put_contents($logFile, "[{$timestamp}] {$logMessage}\n", FILE_APPEND | LOCK_EX); } From 33aa32d1e667676f69e141dfc16f4e589850cff4 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 29 Sep 2025 14:04:14 +0200 Subject: [PATCH 04/28] chore(versions): update coolify version to 4.0.0-beta.433 and nightly version to 4.0.0-beta.434 in configuration files --- config/constants.php | 2 +- other/nightly/versions.json | 4 ++-- versions.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/constants.php b/config/constants.php index ea73d426a..749d6435b 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.432', + 'version' => '4.0.0-beta.433', 'helper_version' => '1.0.11', 'realtime_version' => '1.0.10', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 3255c215b..b5cf3360a 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.432" + "version": "4.0.0-beta.433" }, "nightly": { - "version": "4.0.0-beta.433" + "version": "4.0.0-beta.434" }, "helper": { "version": "1.0.11" diff --git a/versions.json b/versions.json index 3255c215b..b5cf3360a 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.432" + "version": "4.0.0-beta.433" }, "nightly": { - "version": "4.0.0-beta.433" + "version": "4.0.0-beta.434" }, "helper": { "version": "1.0.11" From 6de181222dcb3a87801a68b88836444f17a25d77 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 29 Sep 2025 14:44:39 +0200 Subject: [PATCH 05/28] fix(ui): correct HTML structure and improve clarity in Docker cleanup options --- .../views/livewire/server/docker-cleanup.blade.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/resources/views/livewire/server/docker-cleanup.blade.php b/resources/views/livewire/server/docker-cleanup.blade.php index c2d33bdda..8e96bc963 100644 --- a/resources/views/livewire/server/docker-cleanup.blade.php +++ b/resources/views/livewire/server/docker-cleanup.blade.php @@ -52,8 +52,7 @@
  • Optionally delete unused volumes (if enabled in advanced options).
  • Optionally remove unused networks (if enabled in advanced options).
  • " - instantSave id="forceDockerCleanup" label="Force Docker Cleanup" - /> + instantSave id="forceDockerCleanup" label="Force Docker Cleanup" /> @@ -61,7 +60,8 @@

    Advanced

    -

    These options can cause permanent data loss and functional issues. Only enable if you fully understand the consequences

    +

    These options can cause permanent data loss and functional issues. Only enable if you fully + understand the consequences.

  • Volumes not attached to running containers will be permanently deleted (volumes from stopped containers are affected).
  • Data stored in deleted volumes cannot be recovered.
  • - " - /> + " /> + " />
    From ef4527ed47c62a5024b8ac60f6db7bdeb64264a6 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 29 Sep 2025 14:44:50 +0200 Subject: [PATCH 06/28] feat(ui): enhance resource operations interface with dynamic selection for cloning and moving resources --- .../shared/resource-operations.blade.php | 228 ++++++++++++------ 1 file changed, 159 insertions(+), 69 deletions(-) diff --git a/resources/views/livewire/project/shared/resource-operations.blade.php b/resources/views/livewire/project/shared/resource-operations.blade.php index be7cbd7dc..3d850f542 100644 --- a/resources/views/livewire/project/shared/resource-operations.blade.php +++ b/resources/views/livewire/project/shared/resource-operations.blade.php @@ -1,76 +1,166 @@

    Resource Operations

    You can easily make different kind of operations on this resource.
    -

    Clone

    -
    To another project / environment on a different / same server.
    -
    -
    - @can('update', $resource) - @foreach ($servers->sortBy('id') as $server) -
    Server: {{ $server->name }}
    - @foreach ($server->destinations() as $destination) - - -
    -
    -
    Network
    -
    {{ $destination->name }}
    -
    -
    -
    -
    - @endforeach - @endforeach - @else - - You don't have permission to clone resources. Contact your team administrator to request access. - - @endcan -
    -
    -

    Move

    -
    Between projects / environments.
    -
    -
    - This resource is currently in the {{ $resource->environment->project->name }} / - {{ $resource->environment->name }} environment. -
    -
    - @can('update', $resource) - @forelse ($projects as $project) -
    Project: {{ $project->name }}
    - @foreach ($project->environments as $environment) - - -
    -
    -
    Environment
    -
    {{ $environment->name }}
    -
    -
    - -
    - @endforeach - @empty -
    No projects found to move to
    - @endforelse +
    +

    Clone Resource

    +
    Duplicate this resource to another server or network destination.
    + + @can('update', $resource) +
    +
    +
    + + +
    + +
    + + +
    +
    + +
    + + Clone Resource + +
    + All configurations will be duplicated to the selected destination. The running application won't be + touched. +
    +
    +
    + @else + + You don't have permission to clone resources. Contact your team administrator to request access. + + @endcan + +

    Move Resource

    +
    Transfer this resource between projects and environments.
    + + @can('update', $resource) + @if ($projects->count() > 0) +
    +
    +
    + + +
    + +
    + + +
    +
    + +
    + + Move Resource + +
    + All configurations will be moved to the selected environment. The running application won't be + touched. +
    +
    +
    @else - - You don't have permission to move resources between projects or environments. Contact your team administrator to request access. - - @endcan -
    +
    No other projects available for moving this resource. +
    + @endif + @else + + You don't have permission to move resources between projects or environments. Contact your team + administrator to request access. + + @endcan
    From e9600159898fa32267497ef20a32981e7550d00d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 29 Sep 2025 10:52:08 +0000 Subject: [PATCH 07/28] docs: update changelog --- CHANGELOG.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04b99c646..3447b223b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,40 @@ # Changelog ## [unreleased] +### 🚀 Features + +- *(application)* Implement order-based pattern matching for watch paths with negation support +- *(github)* Enhance Docker Compose input fields for better user experience +- *(dev-seeders)* Add PersonalAccessTokenSeeder to create development API tokens +- *(application)* Add conditional .env file creation for Symfony apps during PHP deployment +- *(application)* Enhance watch path parsing to support negation syntax +- *(application)* Add normalizeWatchPaths method to improve watch path handling +- *(validation)* Enhance ValidGitRepositoryUrl to support additional safe characters and add comprehensive unit tests for various Git repository URL formats +- *(deployment)* Implement detection for Laravel/Symfony frameworks and configure NIXPACKS PHP environment variables accordingly + ### 🐛 Bug Fixes -- *(docker)* Adjust openssh-client installation in Dockerfile to avoid version bug -- *(docker)* Streamline openssh-client installation in Dockerfile +- *(application)* Restrict GitHub-based application settings to non-public repositories +- *(traits)* Update saved_outputs handling in ExecuteRemoteCommand to use collection methods for better performance +- *(application)* Enhance domain handling by replacing both dots and dashes with underscores for HTML form binding +- *(constants)* Reduce command timeout from 7200 to 3600 seconds for improved performance +- *(github)* Update repository URL to point to the v4.x branch for development +- *(models)* Update sorting of scheduled database backups to order by creation date instead of name +- *(socialite)* Add custom base URL support for GitLab provider in OAuth settings +- *(configuration-checker)* Update message to clarify redeployment requirement for configuration changes +- *(application)* Reduce docker stop timeout from 30 to 10 seconds for improved application shutdown efficiency +- *(application)* Increase docker stop timeout from 10 to 30 seconds for better application shutdown handling +- *(validation)* Update git:// URL validation to support port numbers and tilde characters in paths +- Resolve scroll lock issue after closing quick search modal with escape key +- Prevent quick search modal duplication from keyboard shortcuts + +### 🚜 Refactor + +- *(tests)* Simplify matchWatchPaths tests and update implementation for better clarity +- *(deployment)* Improve environment variable handling in ApplicationDeploymentJob +- *(deployment)* Remove commented-out code and streamline environment variable handling in ApplicationDeploymentJob +- *(application)* Improve handling of docker compose domains by normalizing keys and ensuring valid JSON structure +- *(forms)* Update wire:model bindings to use 'blur' instead of 'blur-sm' for input fields across multiple views ### 📚 Documentation @@ -15,20 +45,58 @@ ### 📚 Documentation ### ⚙️ Miscellaneous Tasks -- *(versions)* Increment coolify version numbers to 4.0.0-beta.431 and 4.0.0-beta.432 in configuration files +- *(application)* Remove debugging statement from loadComposeFile method +- *(workflows)* Update Claude GitHub Action configuration to support new event types and improve permissions + +## [4.0.0-beta.431] - 2025-09-24 + +### 📚 Documentation + +- Update changelog ## [4.0.0-beta.430] - 2025-09-24 +### 🚀 Features + +- *(add-watch-paths-for-services)* Show watch paths field for docker compose applications + ### 🐛 Bug Fixes - *(PreviewCompose)* Adds port to preview urls - *(deployment-job)* Enhance build time variable analysis +- *(docker)* Adjust openssh-client installation in Dockerfile to avoid version bug +- *(docker)* Streamline openssh-client installation in Dockerfile +- *(team)* Normalize email case in invite link generation +- *(README)* Update Juxtdigital description to reflect current services +- *(environment-variable-warning)* Enhance warning logic to check for problematic variable values +- *(install)* Ensure proper quoting of environment file paths to prevent issues with spaces +- *(security)* Implement authorization checks for terminal access management +- *(ui)* Improve mobile sidebar close behavior + +### 🚜 Refactor + +- *(installer)* Improve install script +- *(upgrade)* Improve upgrade script +- *(installer, upgrade)* Enhance environment variable management +- *(upgrade)* Enhance logging and quoting in upgrade scripts +- *(upgrade)* Replace warning div with a callout component for better UI consistency +- *(ui)* Replace warning and error divs with callout components for improved consistency and readability +- *(ui)* Improve styling and consistency in environment variable warning and docker cleanup components +- *(security)* Streamline update check functionality and improve UI button interactions in patches view ### 📚 Documentation - Update changelog - Update changelog +### ⚙️ Miscellaneous Tasks + +- *(versions)* Increment coolify version numbers to 4.0.0-beta.431 and 4.0.0-beta.432 in configuration files +- *(versions)* Update coolify version numbers to 4.0.0-beta.432 and 4.0.0-beta.433 in configuration files +- Remove unused files +- Adjust wording +- *(workflow)* Update pull request trigger to pull_request_target and refine permissions for enhanced security + ## [4.0.0-beta.429] - 2025-09-23 ### 🚀 Features @@ -39,6 +107,7 @@ ### 🚀 Features - *(search)* Enable query logging for global search caching - *(environment)* Add dynamic checkbox options for environment variable settings based on user permissions and variable types - *(redaction)* Implement sensitive information redaction in logs and commands +- Improve detection of special network modes - *(api)* Add endpoint to update backup configuration by UUID and backup ID; modify response to include backup id - *(databases)* Enhance backup management API with new endpoints and improved data handling - *(github)* Add GitHub app management endpoints @@ -119,7 +188,6 @@ ## [4.0.0-beta.427] - 2025-09-15 ### 🚀 Features -- Improve detection of special network modes - *(command)* Add option to sync GitHub releases to BunnyCDN and refactor sync logic - *(ui)* Display current version in settings dropdown and update UI accordingly - *(settings)* Add option to restrict PR deployments to repository members and contributors From 5fcc3a32348c33821ced50641cc44565968203cd Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:06:49 +0200 Subject: [PATCH 08/28] fix(workflows): update CLAUDE API key reference in GitHub Actions workflow --- .github/workflows/claude.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index b0fc41448..bf0e6bedc 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -38,7 +38,7 @@ jobs: id: claude uses: anthropics/claude-code-action@v1 with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + anthropic_api_key: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # Optional: Customize the trigger phrase (default: @claude) # trigger_phrase: "/claude" From 323bff5632fbc35947734de036f76d676b7512d5 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:11:18 +0200 Subject: [PATCH 09/28] Update claude.yml --- .github/workflows/claude.yml | 49 +++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index bf0e6bedc..9daf0e90e 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -6,9 +6,7 @@ on: pull_request_review_comment: types: [created] issues: - types: [opened, assigned, labeled] - pull_request: - types: [labeled] + types: [opened, assigned] pull_request_review: types: [submitted] @@ -20,12 +18,12 @@ jobs: (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || (github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'Claude') || (github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'Claude') || - (github.event_name == 'issues' && github.event.action != 'labeled' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) runs-on: ubuntu-latest permissions: - contents: write - pull-requests: write - issues: write + contents: read + pull-requests: read + issues: read id-token: write actions: read # Required for Claude to read CI results on PRs steps: @@ -40,23 +38,28 @@ jobs: with: anthropic_api_key: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1) + # model: "claude-opus-4-1-20250805" + # Optional: Customize the trigger phrase (default: @claude) # trigger_phrase: "/claude" - + # Optional: Trigger when specific user is assigned to an issue # assignee_trigger: "claude-bot" - - # Optional: Configure Claude's behavior with CLI arguments - # claude_args: | - # --model claude-opus-4-1-20250805 - # --max-turns 10 - # --allowedTools "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)" - # --system-prompt "Follow our coding standards. Ensure all new code has tests. Use TypeScript for new files." - - # Optional: Advanced settings configuration - # settings: | - # { - # "env": { - # "NODE_ENV": "test" - # } - # } \ No newline at end of file + + # Optional: Allow Claude to run specific commands + # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)" + + # Optional: Add custom instructions for Claude to customize its behavior for your project + # custom_instructions: | + # Follow our coding standards + # Ensure all new code has tests + # Use TypeScript for new files + + # Optional: Custom environment variables for Claude + # claude_env: | + # NODE_ENV: test From db2d44ca1ff1c86de05937873dd41c84d2aab31d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:19:39 +0200 Subject: [PATCH 10/28] fix(api): correct OpenAPI schema annotations for array items - Replace OA\Schema with OA\Items for array items in DatabasesController - Replace OA\Items with OA\Schema for array type properties in GithubController - Update generated OpenAPI documentation files (openapi.json and openapi.yaml) --- .../Controllers/Api/DatabasesController.php | 2 +- app/Http/Controllers/Api/GithubController.php | 8 +- openapi.json | 887 ++++++++++++++++++ openapi.yaml | 559 +++++++++++ 4 files changed, 1451 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 0e282fccd..5871f481a 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -2173,7 +2173,7 @@ public function delete_execution_by_uuid(Request $request) properties: [ 'executions' => new OA\Schema( type: 'array', - items: new OA\Schema( + items: new OA\Items( type: 'object', properties: [ 'uuid' => ['type' => 'string'], diff --git a/app/Http/Controllers/Api/GithubController.php b/app/Http/Controllers/Api/GithubController.php index 8c95a585f..8c8c87238 100644 --- a/app/Http/Controllers/Api/GithubController.php +++ b/app/Http/Controllers/Api/GithubController.php @@ -219,9 +219,9 @@ public function create_github_app(Request $request) schema: new OA\Schema( type: 'object', properties: [ - 'repositories' => new OA\Items( + 'repositories' => new OA\Schema( type: 'array', - items: new OA\Schema(type: 'object') + items: new OA\Items(type: 'object') ), ] ) @@ -335,9 +335,9 @@ public function load_repositories($github_app_id) schema: new OA\Schema( type: 'object', properties: [ - 'branches' => new OA\Items( + 'branches' => new OA\Schema( type: 'array', - items: new OA\Schema(type: 'object') + items: new OA\Items(type: 'object') ), ] ) diff --git a/openapi.json b/openapi.json index 2b0a81c6e..901741dd0 100644 --- a/openapi.json +++ b/openapi.json @@ -3309,6 +3309,55 @@ ] } }, + "\/databases\/{uuid}\/backups": { + "get": { + "tags": [ + "Databases" + ], + "summary": "Get", + "description": "Get backups details by database UUID.", + "operationId": "get-database-backups-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the database.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Get all backups for a database", + "content": { + "application\/json": { + "schema": { + "type": "string" + }, + "example": "Content is very complex. Will be implemented later." + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "\/databases\/{uuid}": { "get": { "tags": [ @@ -3658,6 +3707,200 @@ ] } }, + "\/databases\/{uuid}\/backups\/{scheduled_backup_uuid}": { + "delete": { + "tags": [ + "Databases" + ], + "summary": "Delete backup configuration", + "description": "Deletes a backup configuration and all its executions.", + "operationId": "delete-backup-configuration-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the database", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "scheduled_backup_uuid", + "in": "path", + "description": "UUID of the backup configuration to delete", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "delete_s3", + "in": "query", + "description": "Whether to delete all backup files from S3", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Backup configuration deleted.", + "content": { + "application\/json": { + "schema": { + "properties": { + "": { + "type": "string", + "example": "Backup configuration and all executions deleted." + } + }, + "type": "object" + } + } + } + }, + "404": { + "description": "Backup configuration not found.", + "content": { + "application\/json": { + "schema": { + "properties": { + "": { + "type": "string", + "example": "Backup configuration not found." + } + }, + "type": "object" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "patch": { + "tags": [ + "Databases" + ], + "summary": "Update", + "description": "Update a specific backup configuration for a given database, identified by its UUID and the backup ID", + "operationId": "update-database-backup", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the database.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "scheduled_backup_uuid", + "in": "path", + "description": "UUID of the backup configuration.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "Database backup configuration data", + "required": true, + "content": { + "application\/json": { + "schema": { + "properties": { + "save_s3": { + "type": "boolean", + "description": "Whether data is saved in s3 or not" + }, + "s3_storage_uuid": { + "type": "string", + "description": "S3 storage UUID" + }, + "backup_now": { + "type": "boolean", + "description": "Whether to take a backup now or not" + }, + "enabled": { + "type": "boolean", + "description": "Whether the backup is enabled or not" + }, + "databases_to_backup": { + "type": "string", + "description": "Comma separated list of databases to backup" + }, + "dump_all": { + "type": "boolean", + "description": "Whether all databases are dumped or not" + }, + "frequency": { + "type": "string", + "description": "Frequency of the backup" + }, + "database_backup_retention_amount_locally": { + "type": "integer", + "description": "Retention amount of the backup locally" + }, + "database_backup_retention_days_locally": { + "type": "integer", + "description": "Retention days of the backup locally" + }, + "database_backup_retention_max_storage_locally": { + "type": "integer", + "description": "Max storage of the backup locally" + }, + "database_backup_retention_amount_s3": { + "type": "integer", + "description": "Retention amount of the backup in s3" + }, + "database_backup_retention_days_s3": { + "type": "integer", + "description": "Retention days of the backup in s3" + }, + "database_backup_retention_max_storage_s3": { + "type": "integer", + "description": "Max storage of the backup in S3" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Database backup configuration updated" + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "\/databases\/postgresql": { "post": { "tags": [ @@ -4694,6 +4937,175 @@ ] } }, + "\/databases\/{uuid}\/backups\/{scheduled_backup_uuid}\/executions\/{execution_uuid}": { + "delete": { + "tags": [ + "Databases" + ], + "summary": "Delete backup execution", + "description": "Deletes a specific backup execution.", + "operationId": "delete-backup-execution-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the database", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "scheduled_backup_uuid", + "in": "path", + "description": "UUID of the backup configuration", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "execution_uuid", + "in": "path", + "description": "UUID of the backup execution to delete", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "delete_s3", + "in": "query", + "description": "Whether to delete the backup from S3", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Backup execution deleted.", + "content": { + "application\/json": { + "schema": { + "properties": { + "": { + "type": "string", + "example": "Backup execution deleted." + } + }, + "type": "object" + } + } + } + }, + "404": { + "description": "Backup execution not found.", + "content": { + "application\/json": { + "schema": { + "properties": { + "": { + "type": "string", + "example": "Backup execution not found." + } + }, + "type": "object" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/databases\/{uuid}\/backups\/{scheduled_backup_uuid}\/executions": { + "get": { + "tags": [ + "Databases" + ], + "summary": "List backup executions", + "description": "Get all executions for a specific backup configuration.", + "operationId": "list-backup-executions", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the database", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "scheduled_backup_uuid", + "in": "path", + "description": "UUID of the backup configuration", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "List of backup executions", + "content": { + "application\/json": { + "schema": { + "properties": { + "": { + "type": "array", + "items": { + "properties": { + "uuid": { + "type": "string" + }, + "filename": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } + }, + "404": { + "description": "Backup configuration not found." + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "\/databases\/{uuid}\/start": { "get": { "tags": [ @@ -5095,6 +5507,477 @@ ] } }, + "\/github-apps": { + "post": { + "tags": [ + "GitHub Apps" + ], + "summary": "Create GitHub App", + "description": "Create a new GitHub app.", + "operationId": "create-github-app", + "requestBody": { + "description": "GitHub app creation payload.", + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "name", + "api_url", + "html_url", + "app_id", + "installation_id", + "client_id", + "client_secret", + "private_key_uuid" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the GitHub app." + }, + "organization": { + "type": "string", + "nullable": true, + "description": "Organization to associate the app with." + }, + "api_url": { + "type": "string", + "description": "API URL for the GitHub app (e.g., https:\/\/api.github.com)." + }, + "html_url": { + "type": "string", + "description": "HTML URL for the GitHub app (e.g., https:\/\/github.com)." + }, + "custom_user": { + "type": "string", + "description": "Custom user for SSH access (default: git)." + }, + "custom_port": { + "type": "integer", + "description": "Custom port for SSH access (default: 22)." + }, + "app_id": { + "type": "integer", + "description": "GitHub App ID from GitHub." + }, + "installation_id": { + "type": "integer", + "description": "GitHub Installation ID." + }, + "client_id": { + "type": "string", + "description": "GitHub OAuth App Client ID." + }, + "client_secret": { + "type": "string", + "description": "GitHub OAuth App Client Secret." + }, + "webhook_secret": { + "type": "string", + "description": "Webhook secret for GitHub webhooks." + }, + "private_key_uuid": { + "type": "string", + "description": "UUID of an existing private key for GitHub App authentication." + }, + "is_system_wide": { + "type": "boolean", + "description": "Is this app system-wide (cloud only)." + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "GitHub app created successfully.", + "content": { + "application\/json": { + "schema": { + "properties": { + "id": { + "type": "integer" + }, + "uuid": { + "type": "string" + }, + "name": { + "type": "string" + }, + "organization": { + "type": "string", + "nullable": true + }, + "api_url": { + "type": "string" + }, + "html_url": { + "type": "string" + }, + "custom_user": { + "type": "string" + }, + "custom_port": { + "type": "integer" + }, + "app_id": { + "type": "integer" + }, + "installation_id": { + "type": "integer" + }, + "client_id": { + "type": "string" + }, + "private_key_id": { + "type": "integer" + }, + "is_system_wide": { + "type": "boolean" + }, + "team_id": { + "type": "integer" + } + }, + "type": "object" + } + } + } + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "422": { + "$ref": "#\/components\/responses\/422" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/github-apps\/{github_app_id}\/repositories": { + "get": { + "tags": [ + "GitHub Apps" + ], + "summary": "Load Repositories for a GitHub App", + "description": "Fetch repositories from GitHub for a given GitHub app.", + "operationId": "load-repositories", + "parameters": [ + { + "name": "github_app_id", + "in": "path", + "description": "GitHub App ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Repositories loaded successfully.", + "content": { + "application\/json": { + "schema": { + "properties": { + "": { + "type": "array", + "items": { + "type": "object" + } + } + }, + "type": "object" + } + } + } + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/github-apps\/{github_app_id}\/repositories\/{owner}\/{repo}\/branches": { + "get": { + "tags": [ + "GitHub Apps" + ], + "summary": "Load Branches for a GitHub Repository", + "description": "Fetch branches from GitHub for a given repository.", + "operationId": "load-branches", + "parameters": [ + { + "name": "github_app_id", + "in": "path", + "description": "GitHub App ID", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "owner", + "in": "path", + "description": "Repository owner", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "repo", + "in": "path", + "description": "Repository name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Branches loaded successfully.", + "content": { + "application\/json": { + "schema": { + "properties": { + "": { + "type": "array", + "items": { + "type": "object" + } + } + }, + "type": "object" + } + } + } + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/github-apps\/{github_app_id}": { + "delete": { + "tags": [ + "GitHub Apps" + ], + "summary": "Delete GitHub App", + "description": "Delete a GitHub app if it's not being used by any applications.", + "operationId": "deleteGithubApp", + "parameters": [ + { + "name": "github_app_id", + "in": "path", + "description": "GitHub App ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "GitHub app deleted successfully", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "GitHub app deleted successfully" + } + }, + "type": "object" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "GitHub app not found" + }, + "409": { + "description": "Conflict - GitHub app is in use", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "This GitHub app is being used by 5 application(s). Please delete all applications first." + } + }, + "type": "object" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "patch": { + "tags": [ + "GitHub Apps" + ], + "summary": "Update GitHub App", + "description": "Update an existing GitHub app.", + "operationId": "updateGithubApp", + "parameters": [ + { + "name": "github_app_id", + "in": "path", + "description": "GitHub App ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application\/json": { + "schema": { + "properties": { + "name": { + "type": "string", + "description": "GitHub App name" + }, + "organization": { + "type": "string", + "nullable": true, + "description": "GitHub organization" + }, + "api_url": { + "type": "string", + "description": "GitHub API URL" + }, + "html_url": { + "type": "string", + "description": "GitHub HTML URL" + }, + "custom_user": { + "type": "string", + "description": "Custom user for SSH" + }, + "custom_port": { + "type": "integer", + "description": "Custom port for SSH" + }, + "app_id": { + "type": "integer", + "description": "GitHub App ID" + }, + "installation_id": { + "type": "integer", + "description": "GitHub Installation ID" + }, + "client_id": { + "type": "string", + "description": "GitHub Client ID" + }, + "client_secret": { + "type": "string", + "description": "GitHub Client Secret" + }, + "webhook_secret": { + "type": "string", + "description": "GitHub Webhook Secret" + }, + "private_key_uuid": { + "type": "string", + "description": "Private key UUID" + }, + "is_system_wide": { + "type": "boolean", + "description": "Is system wide (non-cloud instances only)" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "GitHub app updated successfully", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "GitHub app updated successfully" + }, + "data": { + "type": "object", + "description": "Updated GitHub app data" + } + }, + "type": "object" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "GitHub app not found" + }, + "422": { + "description": "Validation error" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "\/version": { "get": { "summary": "Version", @@ -8890,6 +9773,10 @@ "name": "Deployments", "description": "Deployments" }, + { + "name": "GitHub Apps", + "description": "GitHub Apps" + }, { "name": "Projects", "description": "Projects" diff --git a/openapi.yaml b/openapi.yaml index 9529fcf87..3e39c5d36 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2097,6 +2097,39 @@ paths: security: - bearerAuth: [] + '/databases/{uuid}/backups': + get: + tags: + - Databases + summary: Get + description: 'Get backups details by database UUID.' + operationId: get-database-backups-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the database.' + required: true + schema: + type: string + format: uuid + responses: + '200': + description: 'Get all backups for a database' + content: + application/json: + schema: + type: string + example: 'Content is very complex. Will be implemented later.' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] '/databases/{uuid}': get: tags: @@ -2347,6 +2380,139 @@ paths: security: - bearerAuth: [] + '/databases/{uuid}/backups/{scheduled_backup_uuid}': + delete: + tags: + - Databases + summary: 'Delete backup configuration' + description: 'Deletes a backup configuration and all its executions.' + operationId: delete-backup-configuration-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the database' + required: true + schema: + type: string + - + name: scheduled_backup_uuid + in: path + description: 'UUID of the backup configuration to delete' + required: true + schema: + type: string + format: uuid + - + name: delete_s3 + in: query + description: 'Whether to delete all backup files from S3' + required: false + schema: + type: boolean + default: false + responses: + '200': + description: 'Backup configuration deleted.' + content: + application/json: + schema: + properties: + '': { type: string, example: 'Backup configuration and all executions deleted.' } + type: object + '404': + description: 'Backup configuration not found.' + content: + application/json: + schema: + properties: + '': { type: string, example: 'Backup configuration not found.' } + type: object + security: + - + bearerAuth: [] + patch: + tags: + - Databases + summary: Update + description: 'Update a specific backup configuration for a given database, identified by its UUID and the backup ID' + operationId: update-database-backup + parameters: + - + name: uuid + in: path + description: 'UUID of the database.' + required: true + schema: + type: string + format: uuid + - + name: scheduled_backup_uuid + in: path + description: 'UUID of the backup configuration.' + required: true + schema: + type: string + format: uuid + requestBody: + description: 'Database backup configuration data' + required: true + content: + application/json: + schema: + properties: + save_s3: + type: boolean + description: 'Whether data is saved in s3 or not' + s3_storage_uuid: + type: string + description: 'S3 storage UUID' + backup_now: + type: boolean + description: 'Whether to take a backup now or not' + enabled: + type: boolean + description: 'Whether the backup is enabled or not' + databases_to_backup: + type: string + description: 'Comma separated list of databases to backup' + dump_all: + type: boolean + description: 'Whether all databases are dumped or not' + frequency: + type: string + description: 'Frequency of the backup' + database_backup_retention_amount_locally: + type: integer + description: 'Retention amount of the backup locally' + database_backup_retention_days_locally: + type: integer + description: 'Retention days of the backup locally' + database_backup_retention_max_storage_locally: + type: integer + description: 'Max storage of the backup locally' + database_backup_retention_amount_s3: + type: integer + description: 'Retention amount of the backup in s3' + database_backup_retention_days_s3: + type: integer + description: 'Retention days of the backup in s3' + database_backup_retention_max_storage_s3: + type: integer + description: 'Max storage of the backup in S3' + type: object + responses: + '200': + description: 'Database backup configuration updated' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] /databases/postgresql: post: tags: @@ -3094,6 +3260,102 @@ paths: security: - bearerAuth: [] + '/databases/{uuid}/backups/{scheduled_backup_uuid}/executions/{execution_uuid}': + delete: + tags: + - Databases + summary: 'Delete backup execution' + description: 'Deletes a specific backup execution.' + operationId: delete-backup-execution-by-uuid + parameters: + - + name: uuid + in: path + description: 'UUID of the database' + required: true + schema: + type: string + - + name: scheduled_backup_uuid + in: path + description: 'UUID of the backup configuration' + required: true + schema: + type: string + format: uuid + - + name: execution_uuid + in: path + description: 'UUID of the backup execution to delete' + required: true + schema: + type: string + format: uuid + - + name: delete_s3 + in: query + description: 'Whether to delete the backup from S3' + required: false + schema: + type: boolean + default: false + responses: + '200': + description: 'Backup execution deleted.' + content: + application/json: + schema: + properties: + '': { type: string, example: 'Backup execution deleted.' } + type: object + '404': + description: 'Backup execution not found.' + content: + application/json: + schema: + properties: + '': { type: string, example: 'Backup execution not found.' } + type: object + security: + - + bearerAuth: [] + '/databases/{uuid}/backups/{scheduled_backup_uuid}/executions': + get: + tags: + - Databases + summary: 'List backup executions' + description: 'Get all executions for a specific backup configuration.' + operationId: list-backup-executions + parameters: + - + name: uuid + in: path + description: 'UUID of the database' + required: true + schema: + type: string + - + name: scheduled_backup_uuid + in: path + description: 'UUID of the backup configuration' + required: true + schema: + type: string + format: uuid + responses: + '200': + description: 'List of backup executions' + content: + application/json: + schema: + properties: + '': { type: array, items: { properties: { uuid: { type: string }, filename: { type: string }, size: { type: integer }, created_at: { type: string }, message: { type: string }, status: { type: string } }, type: object } } + type: object + '404': + description: 'Backup configuration not found.' + security: + - + bearerAuth: [] '/databases/{uuid}/start': get: tags: @@ -3348,6 +3610,300 @@ paths: security: - bearerAuth: [] + /github-apps: + post: + tags: + - 'GitHub Apps' + summary: 'Create GitHub App' + description: 'Create a new GitHub app.' + operationId: create-github-app + requestBody: + description: 'GitHub app creation payload.' + required: true + content: + application/json: + schema: + required: + - name + - api_url + - html_url + - app_id + - installation_id + - client_id + - client_secret + - private_key_uuid + properties: + name: + type: string + description: 'Name of the GitHub app.' + organization: + type: string + nullable: true + description: 'Organization to associate the app with.' + api_url: + type: string + description: 'API URL for the GitHub app (e.g., https://api.github.com).' + html_url: + type: string + description: 'HTML URL for the GitHub app (e.g., https://github.com).' + custom_user: + type: string + description: 'Custom user for SSH access (default: git).' + custom_port: + type: integer + description: 'Custom port for SSH access (default: 22).' + app_id: + type: integer + description: 'GitHub App ID from GitHub.' + installation_id: + type: integer + description: 'GitHub Installation ID.' + client_id: + type: string + description: 'GitHub OAuth App Client ID.' + client_secret: + type: string + description: 'GitHub OAuth App Client Secret.' + webhook_secret: + type: string + description: 'Webhook secret for GitHub webhooks.' + private_key_uuid: + type: string + description: 'UUID of an existing private key for GitHub App authentication.' + is_system_wide: + type: boolean + description: 'Is this app system-wide (cloud only).' + type: object + responses: + '201': + description: 'GitHub app created successfully.' + content: + application/json: + schema: + properties: + id: { type: integer } + uuid: { type: string } + name: { type: string } + organization: { type: string, nullable: true } + api_url: { type: string } + html_url: { type: string } + custom_user: { type: string } + custom_port: { type: integer } + app_id: { type: integer } + installation_id: { type: integer } + client_id: { type: string } + private_key_id: { type: integer } + is_system_wide: { type: boolean } + team_id: { type: integer } + type: object + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '422': + $ref: '#/components/responses/422' + security: + - + bearerAuth: [] + '/github-apps/{github_app_id}/repositories': + get: + tags: + - 'GitHub Apps' + summary: 'Load Repositories for a GitHub App' + description: 'Fetch repositories from GitHub for a given GitHub app.' + operationId: load-repositories + parameters: + - + name: github_app_id + in: path + description: 'GitHub App ID' + required: true + schema: + type: integer + responses: + '200': + description: 'Repositories loaded successfully.' + content: + application/json: + schema: + properties: + '': { type: array, items: { type: object } } + type: object + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/github-apps/{github_app_id}/repositories/{owner}/{repo}/branches': + get: + tags: + - 'GitHub Apps' + summary: 'Load Branches for a GitHub Repository' + description: 'Fetch branches from GitHub for a given repository.' + operationId: load-branches + parameters: + - + name: github_app_id + in: path + description: 'GitHub App ID' + required: true + schema: + type: integer + - + name: owner + in: path + description: 'Repository owner' + required: true + schema: + type: string + - + name: repo + in: path + description: 'Repository name' + required: true + schema: + type: string + responses: + '200': + description: 'Branches loaded successfully.' + content: + application/json: + schema: + properties: + '': { type: array, items: { type: object } } + type: object + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] + '/github-apps/{github_app_id}': + delete: + tags: + - 'GitHub Apps' + summary: 'Delete GitHub App' + description: "Delete a GitHub app if it's not being used by any applications." + operationId: deleteGithubApp + parameters: + - + name: github_app_id + in: path + description: 'GitHub App ID' + required: true + schema: + type: integer + responses: + '200': + description: 'GitHub app deleted successfully' + content: + application/json: + schema: + properties: + message: { type: string, example: 'GitHub app deleted successfully' } + type: object + '401': + description: Unauthorized + '404': + description: 'GitHub app not found' + '409': + description: 'Conflict - GitHub app is in use' + content: + application/json: + schema: + properties: + message: { type: string, example: 'This GitHub app is being used by 5 application(s). Please delete all applications first.' } + type: object + security: + - + bearerAuth: [] + patch: + tags: + - 'GitHub Apps' + summary: 'Update GitHub App' + description: 'Update an existing GitHub app.' + operationId: updateGithubApp + parameters: + - + name: github_app_id + in: path + description: 'GitHub App ID' + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + properties: + name: + type: string + description: 'GitHub App name' + organization: + type: string + nullable: true + description: 'GitHub organization' + api_url: + type: string + description: 'GitHub API URL' + html_url: + type: string + description: 'GitHub HTML URL' + custom_user: + type: string + description: 'Custom user for SSH' + custom_port: + type: integer + description: 'Custom port for SSH' + app_id: + type: integer + description: 'GitHub App ID' + installation_id: + type: integer + description: 'GitHub Installation ID' + client_id: + type: string + description: 'GitHub Client ID' + client_secret: + type: string + description: 'GitHub Client Secret' + webhook_secret: + type: string + description: 'GitHub Webhook Secret' + private_key_uuid: + type: string + description: 'Private key UUID' + is_system_wide: + type: boolean + description: 'Is system wide (non-cloud instances only)' + type: object + responses: + '200': + description: 'GitHub app updated successfully' + content: + application/json: + schema: + properties: + message: { type: string, example: 'GitHub app updated successfully' } + data: { type: object, description: 'Updated GitHub app data' } + type: object + '401': + description: Unauthorized + '404': + description: 'GitHub app not found' + '422': + description: 'Validation error' + security: + - + bearerAuth: [] /version: get: summary: Version @@ -5781,6 +6337,9 @@ tags: - name: Deployments description: Deployments + - + name: 'GitHub Apps' + description: 'GitHub Apps' - name: Projects description: Projects From a03c1b3b4b69e0f21a24f136fd79e1aa5a1dfcaf Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:43:30 +0200 Subject: [PATCH 11/28] refactor(dashboard): remove deployment loading logic and introduce DeploymentsIndicator component for better UI management --- app/Livewire/Dashboard.php | 33 ------- app/Livewire/DeploymentsIndicator.php | 49 ++++++++++ .../Project/Application/PreviewsCompose.php | 2 +- app/Livewire/Project/Shared/HealthChecks.php | 6 +- app/Livewire/Server/Advanced.php | 5 - app/Traits/ExecuteRemoteCommand.php | 2 +- bootstrap/helpers/socialite.php | 2 +- database/factories/TeamFactory.php | 4 +- resources/views/layouts/app.blade.php | 1 + resources/views/livewire/dashboard.blade.php | 48 ---------- .../livewire/deployments-indicator.blade.php | 92 +++++++++++++++++++ 11 files changed, 150 insertions(+), 94 deletions(-) create mode 100644 app/Livewire/DeploymentsIndicator.php create mode 100644 resources/views/livewire/deployments-indicator.blade.php diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php index 18dbde0d3..45781af30 100644 --- a/app/Livewire/Dashboard.php +++ b/app/Livewire/Dashboard.php @@ -2,13 +2,10 @@ namespace App\Livewire; -use App\Models\Application; -use App\Models\ApplicationDeploymentQueue; use App\Models\PrivateKey; use App\Models\Project; use App\Models\Server; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Artisan; use Livewire\Component; class Dashboard extends Component @@ -19,41 +16,11 @@ class Dashboard extends Component public Collection $privateKeys; - public array $deploymentsPerServer = []; - public function mount() { $this->privateKeys = PrivateKey::ownedByCurrentTeam()->get(); $this->servers = Server::ownedByCurrentTeam()->get(); $this->projects = Project::ownedByCurrentTeam()->get(); - $this->loadDeployments(); - } - - public function cleanupQueue() - { - try { - $this->authorize('cleanupDeploymentQueue', Application::class); - } catch (\Illuminate\Auth\Access\AuthorizationException $e) { - return handleError($e, $this); - } - - Artisan::queue('cleanup:deployment-queue', [ - '--team-id' => currentTeam()->id, - ]); - } - - public function loadDeployments() - { - $this->deploymentsPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $this->servers->pluck('id'))->get([ - 'id', - 'application_id', - 'application_name', - 'deployment_url', - 'pull_request_id', - 'server_name', - 'server_id', - 'status', - ])->sortBy('id')->groupBy('server_name')->toArray(); } public function navigateToProject($projectUuid) diff --git a/app/Livewire/DeploymentsIndicator.php b/app/Livewire/DeploymentsIndicator.php new file mode 100644 index 000000000..34529a7e7 --- /dev/null +++ b/app/Livewire/DeploymentsIndicator.php @@ -0,0 +1,49 @@ +get(); + + return ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued']) + ->whereIn('server_id', $servers->pluck('id')) + ->get([ + 'id', + 'application_id', + 'application_name', + 'deployment_url', + 'pull_request_id', + 'server_name', + 'server_id', + 'status', + ]) + ->sortBy('id'); + } + + #[Computed] + public function deploymentCount() + { + return $this->deployments->count(); + } + + public function toggleExpanded() + { + $this->expanded = ! $this->expanded; + } + + public function render() + { + return view('livewire.deployments-indicator'); + } +} diff --git a/app/Livewire/Project/Application/PreviewsCompose.php b/app/Livewire/Project/Application/PreviewsCompose.php index 7641edcc5..cfb364b6d 100644 --- a/app/Livewire/Project/Application/PreviewsCompose.php +++ b/app/Livewire/Project/Application/PreviewsCompose.php @@ -73,7 +73,7 @@ public function generate() $host = $url->getHost(); $schema = $url->getScheme(); $portInt = $url->getPort(); - $port = $portInt !== null ? ':' . $portInt : ''; + $port = $portInt !== null ? ':'.$portInt : ''; $random = new Cuid2; $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); diff --git a/app/Livewire/Project/Shared/HealthChecks.php b/app/Livewire/Project/Shared/HealthChecks.php index ee11c496d..c0714fe03 100644 --- a/app/Livewire/Project/Shared/HealthChecks.php +++ b/app/Livewire/Project/Shared/HealthChecks.php @@ -52,13 +52,13 @@ public function toggleHealthcheck() try { $this->authorize('update', $this->resource); $wasEnabled = $this->resource->health_check_enabled; - $this->resource->health_check_enabled = !$this->resource->health_check_enabled; + $this->resource->health_check_enabled = ! $this->resource->health_check_enabled; $this->resource->save(); - if ($this->resource->health_check_enabled && !$wasEnabled && $this->resource->isRunning()) { + if ($this->resource->health_check_enabled && ! $wasEnabled && $this->resource->isRunning()) { $this->dispatch('info', 'Health check has been enabled. A restart is required to apply the new settings.'); } else { - $this->dispatch('success', 'Health check ' . ($this->resource->health_check_enabled ? 'enabled' : 'disabled') . '.'); + $this->dispatch('success', 'Health check '.($this->resource->health_check_enabled ? 'enabled' : 'disabled').'.'); } } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Server/Advanced.php b/app/Livewire/Server/Advanced.php index bbc3bd96a..8d17bb557 100644 --- a/app/Livewire/Server/Advanced.php +++ b/app/Livewire/Server/Advanced.php @@ -2,10 +2,7 @@ namespace App\Livewire\Server; -use App\Models\InstanceSettings; use App\Models\Server; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Hash; use Livewire\Attributes\Validate; use Livewire\Component; @@ -39,8 +36,6 @@ public function mount(string $server_uuid) } } - - public function syncData(bool $toModel = false) { if ($toModel) { diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index 8fa47f543..4aa5aae8b 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -269,4 +269,4 @@ private function addRetryLogEntry(int $attempt, int $maxRetries, int $delay, str $this->application_deployment_queue->save(); } -} \ No newline at end of file +} diff --git a/bootstrap/helpers/socialite.php b/bootstrap/helpers/socialite.php index 3b20f2d89..fd3fbe74b 100644 --- a/bootstrap/helpers/socialite.php +++ b/bootstrap/helpers/socialite.php @@ -75,7 +75,7 @@ function get_socialite_provider(string $provider) $config ); - if ($provider == 'gitlab' && !empty($oauth_setting->base_url)) { + if ($provider == 'gitlab' && ! empty($oauth_setting->base_url)) { $socialite->setHost($oauth_setting->base_url); } diff --git a/database/factories/TeamFactory.php b/database/factories/TeamFactory.php index 0e95842b4..26748c54e 100644 --- a/database/factories/TeamFactory.php +++ b/database/factories/TeamFactory.php @@ -20,7 +20,7 @@ class TeamFactory extends Factory public function definition(): array { return [ - 'name' => $this->faker->company() . ' Team', + 'name' => $this->faker->company().' Team', 'description' => $this->faker->sentence(), 'personal_team' => false, 'show_boarding' => false, @@ -34,7 +34,7 @@ public function personal(): static { return $this->state(fn (array $attributes) => [ 'personal_team' => true, - 'name' => $this->faker->firstName() . "'s Team", + 'name' => $this->faker->firstName()."'s Team", ]); } } diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index bb6533932..409f66ad9 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -7,6 +7,7 @@ @auth +
    -

    Deployments

    - @if (count($deploymentsPerServer) > 0) - - @endif - @can('cleanupDeploymentQueue', Application::class) - - @endcan -
    - - - @endif
    diff --git a/resources/views/livewire/deployments-indicator.blade.php b/resources/views/livewire/deployments-indicator.blade.php new file mode 100644 index 000000000..0ebaab9f8 --- /dev/null +++ b/resources/views/livewire/deployments-indicator.blade.php @@ -0,0 +1,92 @@ +
    + @if ($this->deploymentCount > 0) + + @endif +
    From a9e1d4cb79052105272dce11461289b1697d5fa0 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:47:39 +0200 Subject: [PATCH 12/28] fix(ui): improve queued deployment status readability in dark mode --- resources/views/livewire/deployments-indicator.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/livewire/deployments-indicator.blade.php b/resources/views/livewire/deployments-indicator.blade.php index 0ebaab9f8..ed24249e0 100644 --- a/resources/views/livewire/deployments-indicator.blade.php +++ b/resources/views/livewire/deployments-indicator.blade.php @@ -53,7 +53,7 @@ class="flex items-start gap-3 p-3 rounded-lg dark:bg-coolgray-200 bg-gray-50 tra @else - {{ str_replace('_', ' ', $deployment->status) }}

    From 8e7c869d2307777b1f45a70fbdfea3efd5950c93 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 30 Sep 2025 12:22:57 +0200 Subject: [PATCH 13/28] fix(git): handle additional repository URL cases for 'tangled' and improve branch assignment logic --- app/Livewire/Project/New/PublicGitRepository.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index 8ec818319..89814ee7f 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -176,13 +176,16 @@ public function loadBranch() str($this->repository_url)->startsWith('http://')) && ! str($this->repository_url)->endsWith('.git') && (! str($this->repository_url)->contains('github.com') || - ! str($this->repository_url)->contains('git.sr.ht')) + ! str($this->repository_url)->contains('git.sr.ht')) && + ! str($this->repository_url)->contains('tangled') ) { + $this->repository_url = $this->repository_url.'.git'; } if (str($this->repository_url)->contains('github.com') && str($this->repository_url)->endsWith('.git')) { $this->repository_url = str($this->repository_url)->beforeLast('.git')->value(); } + } catch (\Throwable $e) { return handleError($e, $this); } @@ -190,6 +193,9 @@ public function loadBranch() $this->branchFound = false; $this->getGitSource(); $this->getBranch(); + if (str($this->repository_url)->contains('tangled')) { + $this->git_branch = 'master'; + } $this->selectedBranch = $this->git_branch; } catch (\Throwable $e) { if ($this->rate_limit_remaining == 0) { From 9b4abe753dc51aff34615fb81902c2fecce02273 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 30 Sep 2025 12:23:04 +0200 Subject: [PATCH 14/28] fix(git): enhance error handling for missing branch information during deployment --- app/Jobs/ApplicationDeploymentJob.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 62fbe2df5..fcdb472ee 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1677,7 +1677,23 @@ private function check_git_if_build_needed() ); } if ($this->saved_outputs->get('git_commit_sha') && ! $this->rollback) { - $this->commit = $this->saved_outputs->get('git_commit_sha')->before("\t"); + $output = $this->saved_outputs->get('git_commit_sha'); + + if ($output->isEmpty() || ! str($output)->contains("\t")) { + $errorMessage = "Failed to find branch '{$local_branch}' in repository.\n\n"; + $errorMessage .= "Please verify:\n"; + $errorMessage .= "- The branch name is correct\n"; + $errorMessage .= "- The branch exists in the repository\n"; + $errorMessage .= "- You have access to the repository\n"; + + if ($this->pull_request_id !== 0) { + $errorMessage .= "- The pull request #{$this->pull_request_id} exists and is accessible\n"; + } + + throw new \RuntimeException($errorMessage); + } + + $this->commit = $output->before("\t"); $this->application_deployment_queue->commit = $this->commit; $this->application_deployment_queue->save(); } From 1fe7df7e38567fc47061301a896e8018444452b1 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 30 Sep 2025 12:33:40 +0200 Subject: [PATCH 15/28] fix(git): trim whitespace from repository, branch, and commit SHA fields - Add automatic trimming in Application model's boot method for git_repository, git_branch, and git_commit_sha fields - Add real-time trimming in Source Livewire component via updated{Property} methods - Refresh component state after save to ensure UI displays trimmed values - Prevents deployment issues caused by accidental whitespace in git configuration --- app/Livewire/Project/Application/Source.php | 18 ++++++++++++++++++ app/Models/Application.php | 9 +++++++++ 2 files changed, 27 insertions(+) diff --git a/app/Livewire/Project/Application/Source.php b/app/Livewire/Project/Application/Source.php index 29be68b6c..ab2517f2b 100644 --- a/app/Livewire/Project/Application/Source.php +++ b/app/Livewire/Project/Application/Source.php @@ -47,6 +47,21 @@ public function mount() } } + public function updatedGitRepository() + { + $this->gitRepository = trim($this->gitRepository); + } + + public function updatedGitBranch() + { + $this->gitBranch = trim($this->gitBranch); + } + + public function updatedGitCommitSha() + { + $this->gitCommitSha = trim($this->gitCommitSha); + } + public function syncData(bool $toModel = false) { if ($toModel) { @@ -57,6 +72,9 @@ public function syncData(bool $toModel = false) 'git_commit_sha' => $this->gitCommitSha, 'private_key_id' => $this->privateKeyId, ]); + // Refresh to get the trimmed values from the model + $this->application->refresh(); + $this->syncData(false); } else { $this->gitRepository = $this->application->git_repository; $this->gitBranch = $this->application->git_branch; diff --git a/app/Models/Application.php b/app/Models/Application.php index 9fffdfcda..4f1796790 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -155,6 +155,15 @@ protected static function booted() if ($application->isDirty('publish_directory')) { $payload['publish_directory'] = str($application->publish_directory)->trim(); } + if ($application->isDirty('git_repository')) { + $payload['git_repository'] = str($application->git_repository)->trim(); + } + if ($application->isDirty('git_branch')) { + $payload['git_branch'] = str($application->git_branch)->trim(); + } + if ($application->isDirty('git_commit_sha')) { + $payload['git_commit_sha'] = str($application->git_commit_sha)->trim(); + } if ($application->isDirty('status')) { $payload['last_online_at'] = now(); } From a897e81566c41c07c545aa70fe5af7257e4be295 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 30 Sep 2025 13:37:03 +0200 Subject: [PATCH 16/28] feat(global-search): integrate projects and environments into global search functionality - Added retrieval and mapping of projects and environments to the global search results. - Enhanced search result structure to include resource counts and descriptions for projects and environments. - Updated the UI to reflect the new search capabilities, improving user experience when searching for resources. --- app/Livewire/GlobalSearch.php | 74 ++++++++- app/Models/Environment.php | 2 + app/Models/Project.php | 2 + app/Traits/ClearsGlobalSearchCache.php | 134 ++++++++++------ .../views/livewire/global-search.blade.php | 150 ++++++++---------- 5 files changed, 231 insertions(+), 131 deletions(-) diff --git a/app/Livewire/GlobalSearch.php b/app/Livewire/GlobalSearch.php index dacc0d4db..15de5d838 100644 --- a/app/Livewire/GlobalSearch.php +++ b/app/Livewire/GlobalSearch.php @@ -3,6 +3,8 @@ namespace App\Livewire; use App\Models\Application; +use App\Models\Environment; +use App\Models\Project; use App\Models\Server; use App\Models\Service; use App\Models\StandaloneClickhouse; @@ -335,11 +337,81 @@ private function loadSearchableItems() ]; }); + // Get all projects + $projects = Project::ownedByCurrentTeam() + ->withCount(['environments', 'applications', 'services']) + ->get() + ->map(function ($project) { + $resourceCount = $project->applications_count + $project->services_count; + $resourceSummary = $resourceCount > 0 + ? "{$resourceCount} resource".($resourceCount !== 1 ? 's' : '') + : 'No resources'; + + return [ + 'id' => $project->id, + 'name' => $project->name, + 'type' => 'project', + 'uuid' => $project->uuid, + 'description' => $project->description, + 'link' => $project->navigateTo(), + 'project' => null, + 'environment' => null, + 'resource_count' => $resourceSummary, + 'environment_count' => $project->environments_count, + 'search_text' => strtolower($project->name.' '.$project->description.' project'), + ]; + }); + + // Get all environments + $environments = Environment::query() + ->whereHas('project', function ($query) { + $query->where('team_id', auth()->user()->currentTeam()->id); + }) + ->with('project') + ->withCount(['applications', 'services']) + ->get() + ->map(function ($environment) { + $resourceCount = $environment->applications_count + $environment->services_count; + $resourceSummary = $resourceCount > 0 + ? "{$resourceCount} resource".($resourceCount !== 1 ? 's' : '') + : 'No resources'; + + // Build description with project context + $descriptionParts = []; + if ($environment->project) { + $descriptionParts[] = "Project: {$environment->project->name}"; + } + if ($environment->description) { + $descriptionParts[] = $environment->description; + } + if (empty($descriptionParts)) { + $descriptionParts[] = $resourceSummary; + } + + return [ + 'id' => $environment->id, + 'name' => $environment->name, + 'type' => 'environment', + 'uuid' => $environment->uuid, + 'description' => implode(' • ', $descriptionParts), + 'link' => route('project.resource.index', [ + 'project_uuid' => $environment->project->uuid, + 'environment_uuid' => $environment->uuid, + ]), + 'project' => $environment->project->name ?? null, + 'environment' => null, + 'resource_count' => $resourceSummary, + 'search_text' => strtolower($environment->name.' '.$environment->description.' '.$environment->project->name.' environment'), + ]; + }); + // Merge all collections $items = $items->merge($applications) ->merge($services) ->merge($databases) - ->merge($servers); + ->merge($servers) + ->merge($projects) + ->merge($environments); return $items->toArray(); }); diff --git a/app/Models/Environment.php b/app/Models/Environment.php index 437be7d87..bfeee01c9 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasSafeStringAttribute; use OpenApi\Attributes as OA; @@ -19,6 +20,7 @@ )] class Environment extends BaseModel { + use ClearsGlobalSearchCache; use HasSafeStringAttribute; protected $guarded = []; diff --git a/app/Models/Project.php b/app/Models/Project.php index 1c46042e3..a9bf76803 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Traits\ClearsGlobalSearchCache; use App\Traits\HasSafeStringAttribute; use OpenApi\Attributes as OA; use Visus\Cuid2\Cuid2; @@ -24,6 +25,7 @@ )] class Project extends BaseModel { + use ClearsGlobalSearchCache; use HasSafeStringAttribute; protected $guarded = []; diff --git a/app/Traits/ClearsGlobalSearchCache.php b/app/Traits/ClearsGlobalSearchCache.php index ae587aa87..b9af70aba 100644 --- a/app/Traits/ClearsGlobalSearchCache.php +++ b/app/Traits/ClearsGlobalSearchCache.php @@ -10,77 +10,119 @@ trait ClearsGlobalSearchCache protected static function bootClearsGlobalSearchCache() { static::saving(function ($model) { - // Only clear cache if searchable fields are being changed - if ($model->hasSearchableChanges()) { - $teamId = $model->getTeamIdForCache(); - if (filled($teamId)) { - GlobalSearch::clearTeamCache($teamId); + try { + // Only clear cache if searchable fields are being changed + if ($model->hasSearchableChanges()) { + $teamId = $model->getTeamIdForCache(); + if (filled($teamId)) { + GlobalSearch::clearTeamCache($teamId); + } } + } catch (\Throwable $e) { + // Silently fail cache clearing - don't break the save operation + ray('Failed to clear global search cache on saving: '.$e->getMessage()); } }); static::created(function ($model) { - // Always clear cache when model is created - $teamId = $model->getTeamIdForCache(); - if (filled($teamId)) { - GlobalSearch::clearTeamCache($teamId); + try { + // Always clear cache when model is created + $teamId = $model->getTeamIdForCache(); + if (filled($teamId)) { + GlobalSearch::clearTeamCache($teamId); + } + } catch (\Throwable $e) { + // Silently fail cache clearing - don't break the create operation + ray('Failed to clear global search cache on creation: '.$e->getMessage()); } }); static::deleted(function ($model) { - // Always clear cache when model is deleted - $teamId = $model->getTeamIdForCache(); - if (filled($teamId)) { - GlobalSearch::clearTeamCache($teamId); + try { + // Always clear cache when model is deleted + $teamId = $model->getTeamIdForCache(); + if (filled($teamId)) { + GlobalSearch::clearTeamCache($teamId); + } + } catch (\Throwable $e) { + // Silently fail cache clearing - don't break the delete operation + ray('Failed to clear global search cache on deletion: '.$e->getMessage()); } }); } private function hasSearchableChanges(): bool { - // Define searchable fields based on model type - $searchableFields = ['name', 'description']; + try { + // Define searchable fields based on model type + $searchableFields = ['name', 'description']; - // Add model-specific searchable fields - if ($this instanceof \App\Models\Application) { - $searchableFields[] = 'fqdn'; - $searchableFields[] = 'docker_compose_domains'; - } elseif ($this instanceof \App\Models\Server) { - $searchableFields[] = 'ip'; - } elseif ($this instanceof \App\Models\Service) { - // Services don't have direct fqdn, but name and description are covered - } - // Database models only have name and description as searchable - - // Check if any searchable field is dirty - foreach ($searchableFields as $field) { - if ($this->isDirty($field)) { - return true; + // Add model-specific searchable fields + if ($this instanceof \App\Models\Application) { + $searchableFields[] = 'fqdn'; + $searchableFields[] = 'docker_compose_domains'; + } elseif ($this instanceof \App\Models\Server) { + $searchableFields[] = 'ip'; + } elseif ($this instanceof \App\Models\Service) { + // Services don't have direct fqdn, but name and description are covered + } elseif ($this instanceof \App\Models\Project || $this instanceof \App\Models\Environment) { + // Projects and environments only have name and description as searchable } - } + // Database models only have name and description as searchable - return false; + // Check if any searchable field is dirty + foreach ($searchableFields as $field) { + // Check if attribute exists before checking if dirty + if (array_key_exists($field, $this->getAttributes()) && $this->isDirty($field)) { + return true; + } + } + + return false; + } catch (\Throwable $e) { + // If checking changes fails, assume changes exist to be safe + ray('Failed to check searchable changes: '.$e->getMessage()); + + return true; + } } private function getTeamIdForCache() { - // For database models, team is accessed through environment.project.team - if (method_exists($this, 'team')) { - if ($this instanceof \App\Models\Server) { - $team = $this->team; - } else { - $team = $this->team(); + try { + // For Project models (has direct team_id) + if ($this instanceof \App\Models\Project) { + return $this->team_id ?? null; } - if (filled($team)) { - return is_object($team) ? $team->id : null; + + // For Environment models (get team_id through project) + if ($this instanceof \App\Models\Environment) { + return $this->project?->team_id; } - } - // For models with direct team_id property - if (property_exists($this, 'team_id') || isset($this->team_id)) { - return $this->team_id; - } + // For database models, team is accessed through environment.project.team + if (method_exists($this, 'team')) { + if ($this instanceof \App\Models\Server) { + $team = $this->team; + } else { + $team = $this->team(); + } + if (filled($team)) { + return is_object($team) ? $team->id : null; + } + } - return null; + // For models with direct team_id property + if (property_exists($this, 'team_id') || isset($this->team_id)) { + return $this->team_id ?? null; + } + + return null; + } catch (\Throwable $e) { + // If we can't determine team ID, return null + ray('Failed to get team ID for cache: '.$e->getMessage()); + + return null; + } } } diff --git a/resources/views/livewire/global-search.blade.php b/resources/views/livewire/global-search.blade.php index 0b9b61da4..2addf6f64 100644 --- a/resources/views/livewire/global-search.blade.php +++ b/resources/views/livewire/global-search.blade.php @@ -80,41 +80,42 @@
    -
    + class="fixed top-0 left-0 z-99 flex items-start justify-center w-screen h-screen pt-[20vh]"> +
    -
    -
    -

    Search

    -
    -
    - - - -
    + + @if (strlen($searchQuery) >= 1) +
    + class="min-h-[200px] items-center justify-center p-8">
    @@ -131,59 +132,52 @@ class="min-h-[330px] items-center justify-center">
    -
    +
    @if (strlen($searchQuery) >= 2 && count($searchResults) > 0) -
    +
    @foreach ($searchResults as $index => $result) -
    -
    -
    - + class="search-result-item block px-4 py-3 hover:bg-neutral-50 dark:hover:bg-coolgray-200 transition-colors focus:outline-none focus:bg-neutral-100 dark:focus:bg-coolgray-200 border-l-2 border-transparent hover:border-coollabs focus:border-coollabs"> +
    +
    +
    + {{ $result['name'] }} - @if ($result['type'] === 'server') - - Server - - @endif -
    -
    - @if (!empty($result['project']) && !empty($result['environment'])) - - {{ $result['project'] }} / {{ $result['environment'] }} - - @endif - @if ($result['type'] === 'application') - + + @if ($result['type'] === 'application') Application - - @elseif ($result['type'] === 'service') - + @elseif ($result['type'] === 'service') Service - - @elseif ($result['type'] === 'database') - + @elseif ($result['type'] === 'database') {{ ucfirst($result['subtype'] ?? 'Database') }} - - @endif + @elseif ($result['type'] === 'server') + Server + @elseif ($result['type'] === 'project') + Project + @elseif ($result['type'] === 'environment') + Environment + @endif +
    - @if (!empty($result['description'])) + @if (!empty($result['project']) && !empty($result['environment']))
    - {{ Str::limit($result['description'], 100) }} + class="text-xs text-neutral-500 dark:text-neutral-400 mb-1"> + {{ $result['project'] }} / {{ $result['environment'] }} +
    + @endif + @if (!empty($result['description'])) +
    + {{ Str::limit($result['description'], 80) }}
    @endif
    + class="shrink-0 h-5 w-5 text-neutral-300 dark:text-neutral-600 self-center" + fill="none" viewBox="0 0 24 24" stroke="currentColor"> @@ -192,41 +186,29 @@ class="shrink-0 ml-2 h-4 w-4 text-neutral-400" fill="none" @endforeach
    @elseif (strlen($searchQuery) >= 2 && count($searchResults) === 0) -
    +
    -

    - No results found for "{{ $searchQuery }}" +

    + No results found

    -

    +

    Try different keywords or check the spelling

    @elseif (strlen($searchQuery) > 0 && strlen($searchQuery) < 2) -
    +

    Type at least 2 characters to search

    - @else -
    -
    -

    - Start typing to search -

    -

    - Search for applications, services, databases, and servers -

    -
    -
    @endif
    -
    + @endif
    -
    - +
    From 890f0765726021f534e9bbd04da21e311ad1c43f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:13:14 +0200 Subject: [PATCH 17/28] refactor(dashboard): replace project navigation method with direct link in UI - Removed the navigateToProject method from the Livewire component. - Updated the dashboard view to use anchor tags for project navigation, enhancing user experience and simplifying the code structure. --- app/Livewire/Dashboard.php | 5 ----- resources/views/livewire/dashboard.blade.php | 12 ++++++------ 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php index 45781af30..a78804a16 100644 --- a/app/Livewire/Dashboard.php +++ b/app/Livewire/Dashboard.php @@ -23,11 +23,6 @@ public function mount() $this->projects = Project::ownedByCurrentTeam()->get(); } - public function navigateToProject($projectUuid) - { - return $this->redirect(collect($this->projects)->firstWhere('uuid', $projectUuid)->navigateTo(), navigate: false); - } - public function render() { return view('livewire.dashboard'); diff --git a/resources/views/livewire/dashboard.blade.php b/resources/views/livewire/dashboard.blade.php index 67beb3aa7..2c581850d 100644 --- a/resources/views/livewire/dashboard.blade.php +++ b/resources/views/livewire/dashboard.blade.php @@ -19,8 +19,8 @@ @if ($projects->count() > 0)
    @foreach ($projects as $project) -
    +
    +
    {{ $project->name }}
    @@ -28,20 +28,20 @@ {{ $project->description }}
    -
    +
    @if ($project->environments->first()) @can('createAnyResource') - - + Add Resource + + Add Resource @endcan @endif @can('update', $project) - Settings From 25a7be23a97e6db3717f10751a1adb1b55f7e6d9 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 1 Oct 2025 08:23:21 +0200 Subject: [PATCH 18/28] ui(storage): enhance file storage management with new properties and UI improvements - Added properties to manage file and directory counts, improving data handling in the Livewire component. - Updated the file storage view to include a tabbed interface for volumes, files, and directories, enhancing user navigation. - Improved UI layout for better readability and user experience, including consistent styling and informative messages. --- app/Livewire/Project/Service/Storage.php | 27 +++- .../project/service/file-storage.blade.php | 134 +++++++++--------- .../project/service/storage.blade.php | 125 ++++++++++++---- .../project/shared/storages/show.blade.php | 2 +- 4 files changed, 191 insertions(+), 97 deletions(-) diff --git a/app/Livewire/Project/Service/Storage.php b/app/Livewire/Project/Service/Storage.php index 26cd54425..fb7da1184 100644 --- a/app/Livewire/Project/Service/Storage.php +++ b/app/Livewire/Project/Service/Storage.php @@ -39,7 +39,32 @@ public function refreshStoragesFromEvent() public function refreshStorages() { $this->fileStorage = $this->resource->fileStorages()->get(); - $this->dispatch('$refresh'); + $this->resource->refresh(); + } + + public function getFilesProperty() + { + return $this->fileStorage->where('is_directory', false); + } + + public function getDirectoriesProperty() + { + return $this->fileStorage->where('is_directory', true); + } + + public function getVolumeCountProperty() + { + return $this->resource->persistentStorages()->count(); + } + + public function getFileCountProperty() + { + return $this->files->count(); + } + + public function getDirectoryCountProperty() + { + return $this->directories->count(); } public function addNewVolume($data) diff --git a/resources/views/livewire/project/service/file-storage.blade.php b/resources/views/livewire/project/service/file-storage.blade.php index aa0ce66a3..3aa24b087 100644 --- a/resources/views/livewire/project/service/file-storage.blade.php +++ b/resources/views/livewire/project/service/file-storage.blade.php @@ -1,73 +1,75 @@ -
    -
    -
    - - +
    +
    +
    +
    + + +
    -
    -
    - @can('update', $resource) -
    - @if ($fileStorage->is_directory) - - - @else - @if (!$fileStorage->is_binary) - + @can('update', $resource) +
    + @if ($fileStorage->is_directory) + + shortConfirmationLabel="Filepath" :confirmWithPassword="false" step2ButtonText="Convert to file" /> + + @else + @if (!$fileStorage->is_binary) + + @endif + Load from server + @endif - Load from server - - @endif -
    - @endcan - @if (!$fileStorage->is_directory) - @can('update', $resource) - @if (data_get($resource, 'settings.is_preserve_repository_enabled')) -
    - -
    - @endif - - @if (!$fileStorage->is_based_on_git && !$fileStorage->is_binary) - Save - @endif - @else - @if (data_get($resource, 'settings.is_preserve_repository_enabled')) -
    - -
    - @endif - +
    @endcan - @endif -
    + @if (!$fileStorage->is_directory) + @can('update', $resource) + @if (data_get($resource, 'settings.is_preserve_repository_enabled')) +
    + +
    + @endif + + @if (!$fileStorage->is_based_on_git && !$fileStorage->is_binary) + Save + @endif + @else + @if (data_get($resource, 'settings.is_preserve_repository_enabled')) +
    + +
    + @endif + + @endcan + @endif + +
    diff --git a/resources/views/livewire/project/service/storage.blade.php b/resources/views/livewire/project/service/storage.blade.php index 41d48f386..56a52a566 100644 --- a/resources/views/livewire/project/service/storage.blade.php +++ b/resources/views/livewire/project/service/storage.blade.php @@ -1,4 +1,4 @@ -
    +
    @if ( $resource->getMorphClass() == 'App\Models\Application' || $resource->getMorphClass() == 'App\Models\StandalonePostgresql' || @@ -9,50 +9,117 @@ $resource->getMorphClass() == 'App\Models\StandaloneClickhouse' || $resource->getMorphClass() == 'App\Models\StandaloneMongodb' || $resource->getMorphClass() == 'App\Models\StandaloneMysql') -
    -

    Storages

    - - @if ($resource?->build_pack !== 'dockercompose') - @can('update', $resource) - - - - @endcan - @endif +
    +
    +

    Storages

    + + @if ($resource?->build_pack !== 'dockercompose') + @can('update', $resource) + + + + @endcan + @endif +
    +
    Persistent storage to preserve data between deployments.
    -
    Persistent storage to preserve data between deployments.
    @if ($resource?->build_pack === 'dockercompose') - Please modify storage layout in your Docker Compose - file or reload the compose file to reread the storage layout. +
    Please modify storage layout in your Docker Compose + file or reload the compose file to reread the storage layout.
    @else @if ($resource->persistentStorages()->get()->count() === 0 && $fileStorage->count() == 0) -
    No storage found.
    +
    No storage found.
    @endif @endif - @if ($resource->persistentStorages()->get()->count() > 0) -

    Volumes

    - - @endif - @if ($fileStorage->count() > 0) -
    - @foreach ($fileStorage as $fs) - - @endforeach + @php + $hasVolumes = $this->volumeCount > 0; + $hasFiles = $this->fileCount > 0; + $hasDirectories = $this->directoryCount > 0; + $defaultTab = $hasVolumes ? 'volumes' : ($hasFiles ? 'files' : 'directories'); + @endphp + + @if ($hasVolumes || $hasFiles || $hasDirectories) +
    + {{-- Tabs Navigation --}} +
    + + + +
    + + {{-- Tab Content --}} +
    + {{-- Volumes Tab --}} +
    + @if ($hasVolumes) + + @else +
    + No volumes configured. +
    + @endif +
    + + {{-- Files Tab --}} +
    + @if ($hasFiles) + @foreach ($this->files as $fs) + + @endforeach + @else +
    + No file mounts configured. +
    + @endif +
    + + {{-- Directories Tab --}} +
    + @if ($hasDirectories) + @foreach ($this->directories as $fs) + + @endforeach + @else +
    + No directory mounts configured. +
    + @endif +
    +
    @endif @else @if ($resource->persistentStorages()->get()->count() > 0) -

    {{ Str::headline($resource->name) }}

    +

    {{ Str::headline($resource->name) }}

    @endif @if ($resource->persistentStorages()->get()->count() > 0) @endif @if ($fileStorage->count() > 0) -
    +
    @foreach ($fileStorage->sort() as $fileStorage) diff --git a/resources/views/livewire/project/shared/storages/show.blade.php b/resources/views/livewire/project/shared/storages/show.blade.php index 569df0c4b..8c0ba0c06 100644 --- a/resources/views/livewire/project/shared/storages/show.blade.php +++ b/resources/views/livewire/project/shared/storages/show.blade.php @@ -1,5 +1,5 @@
    -
    + @if ($isReadOnly) @if ($isFirst)
    From bed7ad833eaa5aea5da532cd4844fd23aa235409 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 1 Oct 2025 08:23:35 +0200 Subject: [PATCH 19/28] ui(core): update projects property type and enhance UI styling - Changed the projects property in the Dashboard component from an array to a Collection for improved data handling. - Added new color variables in CSS for better theming options. - Updated button styles across various components for consistency and improved user experience. - Refined dropdown and notification components for better visual alignment and usability. --- app/Livewire/Dashboard.php | 2 +- resources/css/app.css | 7 +++++-- resources/css/utilities.css | 8 ++++---- resources/views/components/dropdown.blade.php | 3 ++- resources/views/livewire/dashboard.blade.php | 2 +- resources/views/livewire/global-search.blade.php | 2 +- resources/views/livewire/notifications/email.blade.php | 4 ++-- .../views/livewire/project/application/rollback.blade.php | 2 +- resources/views/livewire/project/index.blade.php | 2 +- .../views/livewire/project/shared/destination.blade.php | 4 ++-- resources/views/livewire/project/show.blade.php | 2 +- resources/views/livewire/settings-dropdown.blade.php | 2 +- 12 files changed, 22 insertions(+), 18 deletions(-) diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php index a78804a16..57ecaa8a2 100644 --- a/app/Livewire/Dashboard.php +++ b/app/Livewire/Dashboard.php @@ -10,7 +10,7 @@ class Dashboard extends Component { - public $projects = []; + public Collection $projects; public Collection $servers; diff --git a/resources/css/app.css b/resources/css/app.css index 77fa2d66b..aad02d71b 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -20,8 +20,11 @@ @theme { --color-warning: #fcd452; --color-success: #16a34a; --color-error: #dc2626; + --color-coollabs-50: #f5f0ff; --color-coollabs: #6b16ed; --color-coollabs-100: #7317ff; + --color-coollabs-200: #5a12c7; + --color-coollabs-300: #4a0fa3; --color-coolgray-100: #181818; --color-coolgray-200: #202020; --color-coolgray-300: #242424; @@ -91,11 +94,11 @@ option { } button[isError]:not(:disabled) { - @apply text-white bg-red-600 hover:bg-red-700; + @apply text-red-800 dark:text-red-300 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-800 hover:bg-red-300 hover:text-white dark:hover:bg-red-800 dark:hover:text-white; } button[isHighlighted]:not(:disabled) { - @apply text-white bg-coollabs hover:bg-coollabs-100; + @apply text-coollabs-200 dark:text-white bg-coollabs-50 dark:bg-coollabs/20 border-coollabs dark:border-coollabs-100 hover:bg-coollabs hover:text-white dark:hover:bg-coollabs-100 dark:hover:text-white; } h1 { diff --git a/resources/css/utilities.css b/resources/css/utilities.css index cbbe2ef8e..c82b26b0c 100644 --- a/resources/css/utilities.css +++ b/resources/css/utilities.css @@ -63,7 +63,7 @@ @utility select { } @utility button { - @apply flex gap-2 justify-center items-center px-2 py-1 text-sm text-black normal-case rounded-sm border outline-0 cursor-pointer bg-neutral-200/50 border-neutral-300 hover:bg-neutral-300 dark:bg-coolgray-200 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-500 dark:border-coolgray-300 hover:text-black disabled:cursor-not-allowed min-w-fit dark:disabled:text-neutral-600 disabled:border-transparent disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300; + @apply flex gap-2 justify-center items-center px-2 h-8 text-sm text-black normal-case rounded-sm border-2 outline-0 cursor-pointer font-medium bg-white border-neutral-200 hover:bg-neutral-100 dark:bg-coolgray-100 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-200 dark:border-coolgray-300 hover:text-black disabled:cursor-not-allowed min-w-fit dark:disabled:text-neutral-600 disabled:border-transparent disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300; } @utility alert-success { @@ -155,15 +155,15 @@ @utility kbd-custom { } @utility box { - @apply relative flex lg:flex-row flex-col p-2 transition-colors cursor-pointer min-h-[4rem] dark:bg-coolgray-100 shadow-sm bg-white border text-black dark:text-white hover:text-black border-neutral-200 dark:border-black hover:bg-neutral-100 dark:hover:bg-coollabs-100 dark:hover:text-white hover:no-underline; + @apply relative flex lg:flex-row flex-col p-2 transition-colors cursor-pointer min-h-[4rem] dark:bg-coolgray-100 shadow-sm bg-white border text-black dark:text-white hover:text-black border-neutral-200 dark:border-coolgray-300 hover:bg-neutral-100 dark:hover:bg-coollabs-100 dark:hover:text-white hover:no-underline rounded-sm; } @utility box-boarding { - @apply flex lg:flex-row flex-col p-2 transition-colors cursor-pointer min-h-[4rem] dark:bg-coolgray-100 dark:text-white bg-neutral-50 border border-neutral-200 dark:border-black hover:bg-neutral-100 dark:hover:bg-coollabs-100 dark:hover:text-white hover:text-black hover:no-underline text-black; + @apply flex lg:flex-row flex-col p-2 transition-colors cursor-pointer min-h-[4rem] dark:bg-coolgray-100 dark:text-white bg-neutral-50 border border-neutral-200 dark:border-coolgray-300 hover:bg-neutral-100 dark:hover:bg-coollabs-100 dark:hover:text-white hover:text-black hover:no-underline text-black rounded-sm; } @utility box-without-bg { - @apply flex p-2 transition-colors dark:hover:text-white hover:no-underline min-h-[4rem] border border-neutral-200 dark:border-black; + @apply flex p-2 transition-colors dark:hover:text-white hover:no-underline min-h-[4rem] border border-neutral-200 dark:border-coolgray-300 rounded-sm; } @utility box-without-bg-without-border { diff --git a/resources/views/components/dropdown.blade.php b/resources/views/components/dropdown.blade.php index cba60c550..666b93dbc 100644 --- a/resources/views/components/dropdown.blade.php +++ b/resources/views/components/dropdown.blade.php @@ -16,7 +16,8 @@ class="inline-flex items-center justify-start pr-8 transition-colors focus:outli
    -
    +
    {{ $slot }}
    diff --git a/resources/views/livewire/dashboard.blade.php b/resources/views/livewire/dashboard.blade.php index 2c581850d..7a0cbec4a 100644 --- a/resources/views/livewire/dashboard.blade.php +++ b/resources/views/livewire/dashboard.blade.php @@ -19,7 +19,7 @@ @if ($projects->count() > 0)
    @foreach ($projects as $project) -
    +
    diff --git a/resources/views/livewire/global-search.blade.php b/resources/views/livewire/global-search.blade.php index 2addf6f64..aae371b84 100644 --- a/resources/views/livewire/global-search.blade.php +++ b/resources/views/livewire/global-search.blade.php @@ -102,7 +102,7 @@ class="fixed top-0 left-0 z-99 flex items-start justify-center w-screen h-screen + class="w-full pl-12 pr-12 py-4 text-base bg-white dark:bg-coolgray-100 border-none rounded-lg shadow-xl ring-1 ring-neutral-200 dark:ring-coolgray-300 focus:ring-2 focus:ring-neutral-400 dark:focus:ring-coolgray-300 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500" />