From 7061eacfa506f92a8868c531fa52533e3563adc6 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 10 Oct 2025 19:37:16 +0200 Subject: [PATCH 01/20] feat: add cloud-init script support for Hetzner server creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds the ability to use cloud-init scripts when creating Hetzner servers through the integration. Users can write custom scripts that will be executed during server initialization, and optionally save these scripts at the team level for future reuse. Key features: - Textarea field for entering cloud-init scripts (bash or cloud-config YAML) - Checkbox to save scripts for later use at team level - Dropdown to load previously saved scripts - Scripts are encrypted in the database - Full validation and authorization checks - Comprehensive unit and feature tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Livewire/Server/New/ByHetzner.php | 47 ++++++++ app/Models/CloudInitScript.php | 34 ++++++ app/Policies/CloudInitScriptPolicy.php | 65 +++++++++++ ...120000_create_cloud_init_scripts_table.php | 33 ++++++ .../livewire/server/new/by-hetzner.blade.php | 33 ++++++ tests/Feature/CloudInitScriptTest.php | 101 ++++++++++++++++++ tests/Unit/CloudInitScriptValidationTest.php | 76 +++++++++++++ 7 files changed, 389 insertions(+) create mode 100644 app/Models/CloudInitScript.php create mode 100644 app/Policies/CloudInitScriptPolicy.php create mode 100644 database/migrations/2025_10_10_120000_create_cloud_init_scripts_table.php create mode 100644 tests/Feature/CloudInitScriptTest.php create mode 100644 tests/Unit/CloudInitScriptValidationTest.php diff --git a/app/Livewire/Server/New/ByHetzner.php b/app/Livewire/Server/New/ByHetzner.php index 696c4ead8..788144417 100644 --- a/app/Livewire/Server/New/ByHetzner.php +++ b/app/Livewire/Server/New/ByHetzner.php @@ -3,6 +3,7 @@ namespace App\Livewire\Server\New; use App\Enums\ProxyTypes; +use App\Models\CloudInitScript; use App\Models\CloudProviderToken; use App\Models\PrivateKey; use App\Models\Server; @@ -62,16 +63,33 @@ class ByHetzner extends Component public bool $enable_ipv6 = true; + public ?string $cloud_init_script = null; + + public bool $save_cloud_init_script = false; + + public ?string $cloud_init_script_name = null; + + public ?string $cloud_init_script_description = null; + + #[Locked] + public Collection $saved_cloud_init_scripts; + public function mount() { $this->authorize('viewAny', CloudProviderToken::class); $this->loadTokens(); + $this->loadSavedCloudInitScripts(); $this->server_name = generate_random_name(); if ($this->private_keys->count() > 0) { $this->private_key_id = $this->private_keys->first()->id; } } + public function loadSavedCloudInitScripts() + { + $this->saved_cloud_init_scripts = CloudInitScript::ownedByCurrentTeam()->get(); + } + public function getListeners() { return [ @@ -135,6 +153,10 @@ protected function rules(): array 'selectedHetznerSshKeyIds.*' => 'integer', 'enable_ipv4' => 'required|boolean', 'enable_ipv6' => 'required|boolean', + 'cloud_init_script' => 'nullable|string', + 'save_cloud_init_script' => 'boolean', + 'cloud_init_script_name' => 'required_if:save_cloud_init_script,true|nullable|string|max:255', + 'cloud_init_script_description' => 'nullable|string', ]); } @@ -372,6 +394,14 @@ public function updatedSelectedImage($value) ray('Image selected', $value); } + public function loadCloudInitScript(?int $scriptId) + { + if ($scriptId) { + $script = CloudInitScript::ownedByCurrentTeam()->findOrFail($scriptId); + $this->cloud_init_script = $script->script; + } + } + private function createHetznerServer(string $token): array { $hetznerService = new HetznerService($token); @@ -439,6 +469,11 @@ private function createHetznerServer(string $token): array ], ]; + // Add cloud-init script if provided + if (! empty($this->cloud_init_script)) { + $params['user_data'] = $this->cloud_init_script; + } + ray('Server creation parameters', $params); // Create server on Hetzner @@ -460,6 +495,18 @@ public function submit() return $this->dispatch('error', 'You have reached the server limit for your subscription.'); } + // Save cloud-init script if requested + if ($this->save_cloud_init_script && ! empty($this->cloud_init_script)) { + $this->authorize('create', CloudInitScript::class); + + CloudInitScript::create([ + 'team_id' => currentTeam()->id, + 'name' => $this->cloud_init_script_name, + 'script' => $this->cloud_init_script, + 'description' => $this->cloud_init_script_description, + ]); + } + $hetznerToken = $this->getHetznerToken(); // Create server on Hetzner diff --git a/app/Models/CloudInitScript.php b/app/Models/CloudInitScript.php new file mode 100644 index 000000000..8d2cf72a6 --- /dev/null +++ b/app/Models/CloudInitScript.php @@ -0,0 +1,34 @@ + 'encrypted', + ]; + } + + public function team() + { + return $this->belongsTo(Team::class); + } + + public static function ownedByCurrentTeam(array $select = ['*']) + { + $selectArray = collect($select)->concat(['id']); + + return self::whereTeamId(currentTeam()->id)->select($selectArray->all()); + } +} diff --git a/app/Policies/CloudInitScriptPolicy.php b/app/Policies/CloudInitScriptPolicy.php new file mode 100644 index 000000000..0be4f2662 --- /dev/null +++ b/app/Policies/CloudInitScriptPolicy.php @@ -0,0 +1,65 @@ +isAdmin(); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, CloudInitScript $cloudInitScript): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, CloudInitScript $cloudInitScript): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, CloudInitScript $cloudInitScript): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, CloudInitScript $cloudInitScript): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, CloudInitScript $cloudInitScript): bool + { + return $user->isAdmin(); + } +} diff --git a/database/migrations/2025_10_10_120000_create_cloud_init_scripts_table.php b/database/migrations/2025_10_10_120000_create_cloud_init_scripts_table.php new file mode 100644 index 000000000..15985d986 --- /dev/null +++ b/database/migrations/2025_10_10_120000_create_cloud_init_scripts_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('team_id')->constrained()->onDelete('cascade'); + $table->string('name'); + $table->text('script'); // Encrypted in the model + $table->text('description')->nullable(); + $table->timestamps(); + + $table->index('team_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cloud_init_scripts'); + } +}; diff --git a/resources/views/livewire/server/new/by-hetzner.blade.php b/resources/views/livewire/server/new/by-hetzner.blade.php index 8f03bc9b7..775aed601 100644 --- a/resources/views/livewire/server/new/by-hetzner.blade.php +++ b/resources/views/livewire/server/new/by-hetzner.blade.php @@ -156,6 +156,39 @@ class="p-4 border border-yellow-500 dark:border-yellow-600 rounded bg-yellow-50 +
+
+ + @if ($saved_cloud_init_scripts->count() > 0) + + + @foreach ($saved_cloud_init_scripts as $script) + + @endforeach + + @endif +
+ + + @if (!empty($cloud_init_script)) +
+ + + @if ($save_cloud_init_script) +
+ + +
+ @endif +
+ @endif +
+
Back diff --git a/tests/Feature/CloudInitScriptTest.php b/tests/Feature/CloudInitScriptTest.php new file mode 100644 index 000000000..881f0071c --- /dev/null +++ b/tests/Feature/CloudInitScriptTest.php @@ -0,0 +1,101 @@ + 'test-server', + 'server_type' => 'cx11', + 'image' => 1, + 'location' => 'nbg1', + 'start_after_create' => true, + 'ssh_keys' => [123], + 'public_net' => [ + 'enable_ipv4' => true, + 'enable_ipv6' => true, + ], + ]; + + // Add cloud-init script if provided + if (! empty($cloudInitScript)) { + $params['user_data'] = $cloudInitScript; + } + + expect($params) + ->toHaveKey('user_data') + ->and($params['user_data'])->toBe("#!/bin/bash\necho 'Hello World'"); +}); + +it('validates cloud-init script is not included when empty', function () { + $cloudInitScript = null; + $params = [ + 'name' => 'test-server', + 'server_type' => 'cx11', + 'image' => 1, + 'location' => 'nbg1', + 'start_after_create' => true, + 'ssh_keys' => [123], + 'public_net' => [ + 'enable_ipv4' => true, + 'enable_ipv6' => true, + ], + ]; + + // Add cloud-init script if provided + if (! empty($cloudInitScript)) { + $params['user_data'] = $cloudInitScript; + } + + expect($params)->not->toHaveKey('user_data'); +}); + +it('validates cloud-init script is not included when empty string', function () { + $cloudInitScript = ''; + $params = [ + 'name' => 'test-server', + 'server_type' => 'cx11', + 'image' => 1, + 'location' => 'nbg1', + 'start_after_create' => true, + 'ssh_keys' => [123], + 'public_net' => [ + 'enable_ipv4' => true, + 'enable_ipv6' => true, + ], + ]; + + // Add cloud-init script if provided + if (! empty($cloudInitScript)) { + $params['user_data'] = $cloudInitScript; + } + + expect($params)->not->toHaveKey('user_data'); +}); + +it('validates cloud-init script with multiline content', function () { + $cloudInitScript = "#cloud-config\n\npackages:\n - nginx\n - git\n\nruncmd:\n - systemctl start nginx"; + $params = [ + 'name' => 'test-server', + 'server_type' => 'cx11', + 'image' => 1, + 'location' => 'nbg1', + 'start_after_create' => true, + 'ssh_keys' => [123], + 'public_net' => [ + 'enable_ipv4' => true, + 'enable_ipv6' => true, + ], + ]; + + // Add cloud-init script if provided + if (! empty($cloudInitScript)) { + $params['user_data'] = $cloudInitScript; + } + + expect($params) + ->toHaveKey('user_data') + ->and($params['user_data'])->toContain('#cloud-config') + ->and($params['user_data'])->toContain('packages:') + ->and($params['user_data'])->toContain('runcmd:'); +}); diff --git a/tests/Unit/CloudInitScriptValidationTest.php b/tests/Unit/CloudInitScriptValidationTest.php new file mode 100644 index 000000000..bb4657502 --- /dev/null +++ b/tests/Unit/CloudInitScriptValidationTest.php @@ -0,0 +1,76 @@ +toBeFalse() + ->and($hasValue)->toBeFalse(); +}); + +it('validates cloud-init script name is required when saving', function () { + $saveScript = true; + $scriptName = 'My Installation Script'; + + $isNameRequired = $saveScript; + $hasName = ! empty($scriptName); + + expect($isNameRequired)->toBeTrue() + ->and($hasName)->toBeTrue(); +}); + +it('validates cloud-init script description is optional', function () { + $scriptDescription = null; + + $isDescriptionRequired = false; + $hasDescription = ! empty($scriptDescription); + + expect($isDescriptionRequired)->toBeFalse() + ->and($hasDescription)->toBeFalse(); +}); + +it('validates save_cloud_init_script must be boolean', function () { + $saveCloudInitScript = true; + + expect($saveCloudInitScript)->toBeBool(); +}); + +it('validates save_cloud_init_script defaults to false', function () { + $saveCloudInitScript = false; + + expect($saveCloudInitScript)->toBeFalse(); +}); + +it('validates cloud-init script can be a bash script', function () { + $cloudInitScript = "#!/bin/bash\napt-get update\napt-get install -y nginx"; + + expect($cloudInitScript)->toBeString() + ->and($cloudInitScript)->toContain('#!/bin/bash'); +}); + +it('validates cloud-init script can be cloud-config yaml', function () { + $cloudInitScript = "#cloud-config\npackages:\n - nginx\n - git"; + + expect($cloudInitScript)->toBeString() + ->and($cloudInitScript)->toContain('#cloud-config'); +}); + +it('validates script name max length is 255 characters', function () { + $scriptName = str_repeat('a', 255); + + expect(strlen($scriptName))->toBe(255) + ->and(strlen($scriptName))->toBeLessThanOrEqual(255); +}); + +it('validates script name exceeding 255 characters should be invalid', function () { + $scriptName = str_repeat('a', 256); + + $isValid = strlen($scriptName) <= 255; + + expect($isValid)->toBeFalse() + ->and(strlen($scriptName))->toBeGreaterThan(255); +}); From c009c97eb10a7ab9443b5cd40687ef15d17c5ffb Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 10 Oct 2025 19:46:27 +0200 Subject: [PATCH 02/20] fix(ci): sanitize branch names for Docker tag compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The staging build workflow was failing because branch names containing slashes (e.g., andrasbacsai/hetzner-cloud-init) were being used directly as Docker tags, which is invalid. This commit adds a sanitization step to replace slashes with dashes, converting branch names like "andrasbacsai/hetzner-cloud-init" to "andrasbacsai-hetzner-cloud-init" for use as Docker tags. Changes: - Add sanitize step to amd64 job - Add sanitize step to aarch64 job - Add sanitize step to merge-manifest job - Replace all ${{ github.ref_name }} with ${{ steps.sanitize.outputs.tag }} 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/coolify-staging-build.yml | 37 ++++++++++++++++----- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/.github/workflows/coolify-staging-build.yml b/.github/workflows/coolify-staging-build.yml index 09b1e9421..c6aa2dd90 100644 --- a/.github/workflows/coolify-staging-build.yml +++ b/.github/workflows/coolify-staging-build.yml @@ -28,6 +28,13 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Sanitize branch name for Docker tag + id: sanitize + run: | + # Replace slashes and other invalid characters with dashes + SANITIZED_NAME=$(echo "${{ github.ref_name }}" | sed 's/[\/]/-/g') + echo "tag=${SANITIZED_NAME}" >> $GITHUB_OUTPUT + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: @@ -50,8 +57,8 @@ jobs: platforms: linux/amd64 push: true tags: | - ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} - ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }} aarch64: runs-on: [self-hosted, arm64] @@ -61,6 +68,13 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Sanitize branch name for Docker tag + id: sanitize + run: | + # Replace slashes and other invalid characters with dashes + SANITIZED_NAME=$(echo "${{ github.ref_name }}" | sed 's/[\/]/-/g') + echo "tag=${SANITIZED_NAME}" >> $GITHUB_OUTPUT + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: @@ -83,8 +97,8 @@ jobs: platforms: linux/aarch64 push: true tags: | - ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 - ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64 merge-manifest: runs-on: ubuntu-latest @@ -95,6 +109,13 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Sanitize branch name for Docker tag + id: sanitize + run: | + # Replace slashes and other invalid characters with dashes + SANITIZED_NAME=$(echo "${{ github.ref_name }}" | sed 's/[\/]/-/g') + echo "tag=${SANITIZED_NAME}" >> $GITHUB_OUTPUT + - uses: docker/setup-buildx-action@v3 - name: Login to ${{ env.GITHUB_REGISTRY }} @@ -114,14 +135,14 @@ jobs: - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | docker buildx imagetools create \ - --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 \ - --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }} - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} run: | docker buildx imagetools create \ - --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 \ - --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }} - uses: sarisia/actions-status-discord@v1 if: always() From 05bd57ed5142d98aca37c6513d466f8abfdcd695 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 11 Oct 2025 10:55:14 +0200 Subject: [PATCH 03/20] refactor(ui): improve cloud-init script save checkbox visibility and styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: - Make save checkbox visible by default (not conditionally rendered) - Disable checkbox when cloud_init_script is empty - Auto-enable when user types in the script textarea (via Livewire reactivity) - Remove unnecessary border wrapper around checkbox for cleaner look - Checkbox styling now matches other checkboxes in the form (no border) This improves UX by making the save option always discoverable while preventing users from checking it when there's no script to save. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../livewire/server/new/by-hetzner.blade.php | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/resources/views/livewire/server/new/by-hetzner.blade.php b/resources/views/livewire/server/new/by-hetzner.blade.php index 775aed601..40ad094da 100644 --- a/resources/views/livewire/server/new/by-hetzner.blade.php +++ b/resources/views/livewire/server/new/by-hetzner.blade.php @@ -173,20 +173,19 @@ class="p-4 border border-yellow-500 dark:border-yellow-600 rounded bg-yellow-50 helper="Add a cloud-init script to run when the server is created. See Hetzner's documentation for details." rows="8" /> - @if (!empty($cloud_init_script)) -
- +
+ - @if ($save_cloud_init_script) -
- - -
- @endif -
- @endif + @if ($save_cloud_init_script) +
+ + +
+ @endif +
From e4bf8ab3374e1b7052a1b2e87e7c9e080f111daa Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 11 Oct 2025 10:59:26 +0200 Subject: [PATCH 04/20] refactor: enable cloud-init save checkbox at all times with backend validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the disabled state from the save checkbox to allow users to check it at any time. The backend already validates that cloud_init_script is not empty before saving (line 499 in ByHetzner.php), so empty/null scripts will simply not be saved even if the checkbox is checked. This improves UX by making the checkbox always interactive while maintaining data integrity through backend validation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- resources/views/livewire/server/new/by-hetzner.blade.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/views/livewire/server/new/by-hetzner.blade.php b/resources/views/livewire/server/new/by-hetzner.blade.php index 40ad094da..a92fcb177 100644 --- a/resources/views/livewire/server/new/by-hetzner.blade.php +++ b/resources/views/livewire/server/new/by-hetzner.blade.php @@ -175,8 +175,7 @@ class="p-4 border border-yellow-500 dark:border-yellow-600 rounded bg-yellow-50
+ helper="Save this cloud-init script to your team's library for reuse" /> @if ($save_cloud_init_script)
From 6c0840d4e0cc1a9924c8b98163b38113290c2c01 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 11 Oct 2025 11:16:28 +0200 Subject: [PATCH 05/20] refactor: improve cloud-init script UX and remove description field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: 1. Remove description field from cloud-init scripts - Updated migration to remove description column - Updated model to remove description from fillable array 2. Redesign script name input layout - Move script name input next to checkbox (always visible) - Remove conditional rendering - input always shown - Use placeholder instead of label for cleaner look 3. Fix dropdown type error - Replace wire:change event with wire:model.live - Use updatedSelectedCloudInitScriptId() lifecycle hook - Add "disabled" attribute to placeholder option - Properly handle empty string vs null in type casting 4. Improve validation - Require both script content AND name for saving - Remove description validation rule - Add selected_cloud_init_script_id validation 5. Auto-populate name when loading saved script - When user selects saved script, auto-fill the name field 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Livewire/Server/New/ByHetzner.php | 16 +++++++-------- app/Models/CloudInitScript.php | 1 - ...120000_create_cloud_init_scripts_table.php | 1 - .../livewire/server/new/by-hetzner.blade.php | 20 +++++++------------ 4 files changed, 15 insertions(+), 23 deletions(-) diff --git a/app/Livewire/Server/New/ByHetzner.php b/app/Livewire/Server/New/ByHetzner.php index 788144417..284e5e790 100644 --- a/app/Livewire/Server/New/ByHetzner.php +++ b/app/Livewire/Server/New/ByHetzner.php @@ -69,7 +69,7 @@ class ByHetzner extends Component public ?string $cloud_init_script_name = null; - public ?string $cloud_init_script_description = null; + public ?int $selected_cloud_init_script_id = null; #[Locked] public Collection $saved_cloud_init_scripts; @@ -155,8 +155,8 @@ protected function rules(): array 'enable_ipv6' => 'required|boolean', 'cloud_init_script' => 'nullable|string', 'save_cloud_init_script' => 'boolean', - 'cloud_init_script_name' => 'required_if:save_cloud_init_script,true|nullable|string|max:255', - 'cloud_init_script_description' => 'nullable|string', + 'cloud_init_script_name' => 'nullable|string|max:255', + 'selected_cloud_init_script_id' => 'nullable|integer|exists:cloud_init_scripts,id', ]); } @@ -394,11 +394,12 @@ public function updatedSelectedImage($value) ray('Image selected', $value); } - public function loadCloudInitScript(?int $scriptId) + public function updatedSelectedCloudInitScriptId($value) { - if ($scriptId) { - $script = CloudInitScript::ownedByCurrentTeam()->findOrFail($scriptId); + if ($value) { + $script = CloudInitScript::ownedByCurrentTeam()->findOrFail($value); $this->cloud_init_script = $script->script; + $this->cloud_init_script_name = $script->name; } } @@ -496,14 +497,13 @@ public function submit() } // Save cloud-init script if requested - if ($this->save_cloud_init_script && ! empty($this->cloud_init_script)) { + if ($this->save_cloud_init_script && ! empty($this->cloud_init_script) && ! empty($this->cloud_init_script_name)) { $this->authorize('create', CloudInitScript::class); CloudInitScript::create([ 'team_id' => currentTeam()->id, 'name' => $this->cloud_init_script_name, 'script' => $this->cloud_init_script, - 'description' => $this->cloud_init_script_description, ]); } diff --git a/app/Models/CloudInitScript.php b/app/Models/CloudInitScript.php index 8d2cf72a6..2c78cc582 100644 --- a/app/Models/CloudInitScript.php +++ b/app/Models/CloudInitScript.php @@ -10,7 +10,6 @@ class CloudInitScript extends Model 'team_id', 'name', 'script', - 'description', ]; protected function casts(): array diff --git a/database/migrations/2025_10_10_120000_create_cloud_init_scripts_table.php b/database/migrations/2025_10_10_120000_create_cloud_init_scripts_table.php index 15985d986..fe216a57d 100644 --- a/database/migrations/2025_10_10_120000_create_cloud_init_scripts_table.php +++ b/database/migrations/2025_10_10_120000_create_cloud_init_scripts_table.php @@ -16,7 +16,6 @@ public function up(): void $table->foreignId('team_id')->constrained()->onDelete('cascade'); $table->string('name'); $table->text('script'); // Encrypted in the model - $table->text('description')->nullable(); $table->timestamps(); $table->index('team_id'); diff --git a/resources/views/livewire/server/new/by-hetzner.blade.php b/resources/views/livewire/server/new/by-hetzner.blade.php index a92fcb177..9df8ccea6 100644 --- a/resources/views/livewire/server/new/by-hetzner.blade.php +++ b/resources/views/livewire/server/new/by-hetzner.blade.php @@ -160,9 +160,9 @@ class="p-4 border border-yellow-500 dark:border-yellow-600 rounded bg-yellow-50
@if ($saved_cloud_init_scripts->count() > 0) - - + @foreach ($saved_cloud_init_scripts as $script) @endforeach @@ -173,17 +173,11 @@ class="p-4 border border-yellow-500 dark:border-yellow-600 rounded bg-yellow-50 helper="Add a cloud-init script to run when the server is created. See Hetzner's documentation for details." rows="8" /> -
- - - @if ($save_cloud_init_script) -
- - -
- @endif +
+ +
+ +
From e055c3b101593f2f36d53138349e5e364598f7d6 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 11 Oct 2025 11:17:44 +0200 Subject: [PATCH 06/20] debug: add ray logging for Hetzner createServer API request/response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add detailed ray logging to track exactly what is being sent to Hetzner's API and what response is received. This will help debug cloud-init script integration and verify that user_data is properly included in the request. Logs include: - Request endpoint and full params object - Complete API response 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Services/HetznerService.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/Services/HetznerService.php b/app/Services/HetznerService.php index 95cd1e8e8..aa6de3897 100644 --- a/app/Services/HetznerService.php +++ b/app/Services/HetznerService.php @@ -108,8 +108,17 @@ public function uploadSshKey(string $name, string $publicKey): array public function createServer(array $params): array { + ray('Hetzner createServer request', [ + 'endpoint' => '/servers', + 'params' => $params, + ]); + $response = $this->request('post', '/servers', $params); + ray('Hetzner createServer response', [ + 'response' => $response, + ]); + return $response['server'] ?? []; } From ad7479b1675758ac389e0f5b94b922a435e03db5 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 11 Oct 2025 11:25:42 +0200 Subject: [PATCH 07/20] fix: set cloud-init script dropdown to empty by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove 'selected' and 'disabled' attributes from the placeholder option to ensure the dropdown shows "Load saved script..." by default instead of appearing to select the first script. When selected_cloud_init_script_id is null, the empty option will be shown. The updatedSelectedCloudInitScriptId() method already handles empty string values correctly by checking if ($value). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../livewire/server/new/by-hetzner.blade.php | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/resources/views/livewire/server/new/by-hetzner.blade.php b/resources/views/livewire/server/new/by-hetzner.blade.php index 9df8ccea6..63f420f3f 100644 --- a/resources/views/livewire/server/new/by-hetzner.blade.php +++ b/resources/views/livewire/server/new/by-hetzner.blade.php @@ -18,7 +18,8 @@
- + Continue
@@ -48,8 +49,7 @@
- + @foreach ($locations as $location)
- + @@ -111,8 +111,7 @@ class="p-4 border border-yellow-500 dark:border-yellow-600 rounded bg-yellow-50

No private keys found. You need to create a private key to continue.

- +
@@ -158,11 +157,10 @@ class="p-4 border border-yellow-500 dark:border-yellow-600 rounded bg-yellow-50
- + @if ($saved_cloud_init_scripts->count() > 0) - - + + @foreach ($saved_cloud_init_scripts as $script) @endforeach @@ -194,4 +192,4 @@ class="p-4 border border-yellow-500 dark:border-yellow-600 rounded bg-yellow-50 @endif @endif @endif -
+
\ No newline at end of file From b31b080799fb85aa8d222263466b79b7d6777974 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 11 Oct 2025 12:37:12 +0200 Subject: [PATCH 08/20] fix: reset cloud-init fields when closing server creation modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add cloud-init script fields to the resetSelection() method that's called when the modal is closed. This ensures a clean slate when reopening the "Connect a Hetzner Server" view. Fields reset: - cloud_init_script - save_cloud_init_script - cloud_init_script_name - selected_cloud_init_script_id 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Livewire/Server/New/ByHetzner.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Livewire/Server/New/ByHetzner.php b/app/Livewire/Server/New/ByHetzner.php index 284e5e790..7d828b12e 100644 --- a/app/Livewire/Server/New/ByHetzner.php +++ b/app/Livewire/Server/New/ByHetzner.php @@ -103,6 +103,10 @@ public function resetSelection() { $this->selected_token_id = null; $this->current_step = 1; + $this->cloud_init_script = null; + $this->save_cloud_init_script = false; + $this->cloud_init_script_name = null; + $this->selected_cloud_init_script_id = null; } public function loadTokens() From 5463f4d4961cfccab6ddbc9fa341c74d151261a0 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 11 Oct 2025 12:42:09 +0200 Subject: [PATCH 09/20] feat: add cloud-init scripts management UI in Security section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive cloud-init script management interface in the Security section, allowing users to create, edit, delete, and reuse cloud-init scripts across their team. New Components: - CloudInitScripts: Main listing page with grid view of scripts - CloudInitScriptForm: Modal form for create/edit operations Features: - Create new cloud-init scripts with name and content - Edit existing scripts - Delete scripts with confirmation (requires typing script name) - View script preview (first 200 characters) - Scripts are encrypted in database - Full authorization using CloudInitScriptPolicy - Real-time updates via Livewire events UI Location: - Added to Security section nav: /security/cloud-init-scripts - Positioned between Cloud Tokens and API Tokens - Follows existing security UI patterns Files Created: - app/Livewire/Security/CloudInitScripts.php - app/Livewire/Security/CloudInitScriptForm.php - resources/views/livewire/security/cloud-init-scripts.blade.php - resources/views/livewire/security/cloud-init-script-form.blade.php Files Modified: - routes/web.php - Added route - resources/views/components/security/navbar.blade.php - Added nav link 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Livewire/Security/CloudInitScriptForm.php | 97 +++++++++++++++++++ app/Livewire/Security/CloudInitScripts.php | 52 ++++++++++ .../components/security/navbar.blade.php | 5 + .../security/cloud-init-script-form.blade.php | 17 ++++ .../security/cloud-init-scripts.blade.php | 57 +++++++++++ routes/web.php | 2 + 6 files changed, 230 insertions(+) create mode 100644 app/Livewire/Security/CloudInitScriptForm.php create mode 100644 app/Livewire/Security/CloudInitScripts.php create mode 100644 resources/views/livewire/security/cloud-init-script-form.blade.php create mode 100644 resources/views/livewire/security/cloud-init-scripts.blade.php diff --git a/app/Livewire/Security/CloudInitScriptForm.php b/app/Livewire/Security/CloudInitScriptForm.php new file mode 100644 index 000000000..5307e28b3 --- /dev/null +++ b/app/Livewire/Security/CloudInitScriptForm.php @@ -0,0 +1,97 @@ +scriptId = $scriptId; + $cloudInitScript = CloudInitScript::ownedByCurrentTeam()->findOrFail($scriptId); + $this->authorize('update', $cloudInitScript); + + $this->name = $cloudInitScript->name; + $this->script = $cloudInitScript->script; + } else { + $this->authorize('create', CloudInitScript::class); + } + } + + protected function rules(): array + { + return [ + 'name' => 'required|string|max:255', + 'script' => 'required|string', + ]; + } + + protected function messages(): array + { + return [ + 'name.required' => 'Script name is required.', + 'name.max' => 'Script name cannot exceed 255 characters.', + 'script.required' => 'Cloud-init script content is required.', + ]; + } + + public function save() + { + $this->validate(); + + try { + if ($this->scriptId) { + // Update existing script + $cloudInitScript = CloudInitScript::ownedByCurrentTeam()->findOrFail($this->scriptId); + $this->authorize('update', $cloudInitScript); + + $cloudInitScript->update([ + 'name' => $this->name, + 'script' => $this->script, + ]); + + $message = 'Cloud-init script updated successfully.'; + } else { + // Create new script + $this->authorize('create', CloudInitScript::class); + + CloudInitScript::create([ + 'team_id' => currentTeam()->id, + 'name' => $this->name, + 'script' => $this->script, + ]); + + $message = 'Cloud-init script created successfully.'; + } + + $this->reset(['name', 'script', 'scriptId']); + $this->dispatch('scriptSaved'); + $this->dispatch('success', $message); + + if ($this->modal_mode) { + $this->dispatch('closeModal'); + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.security.cloud-init-script-form'); + } +} diff --git a/app/Livewire/Security/CloudInitScripts.php b/app/Livewire/Security/CloudInitScripts.php new file mode 100644 index 000000000..13bcf2caa --- /dev/null +++ b/app/Livewire/Security/CloudInitScripts.php @@ -0,0 +1,52 @@ +authorize('viewAny', CloudInitScript::class); + $this->loadScripts(); + } + + public function getListeners() + { + return [ + 'scriptSaved' => 'loadScripts', + ]; + } + + public function loadScripts() + { + $this->scripts = CloudInitScript::ownedByCurrentTeam()->orderBy('created_at', 'desc')->get(); + } + + public function deleteScript(int $scriptId) + { + try { + $script = CloudInitScript::ownedByCurrentTeam()->findOrFail($scriptId); + $this->authorize('delete', $script); + + $script->delete(); + $this->loadScripts(); + + $this->dispatch('success', 'Cloud-init script deleted successfully.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.security.cloud-init-scripts'); + } +} diff --git a/resources/views/components/security/navbar.blade.php b/resources/views/components/security/navbar.blade.php index b0dfdd242..425c96d74 100644 --- a/resources/views/components/security/navbar.blade.php +++ b/resources/views/components/security/navbar.blade.php @@ -11,6 +11,11 @@ @endcan + @can('viewAny', App\Models\CloudInitScript::class) + + + + @endcan diff --git a/resources/views/livewire/security/cloud-init-script-form.blade.php b/resources/views/livewire/security/cloud-init-script-form.blade.php new file mode 100644 index 000000000..545c49a7f --- /dev/null +++ b/resources/views/livewire/security/cloud-init-script-form.blade.php @@ -0,0 +1,17 @@ +
+ + + + +
+ @if ($modal_mode) + + Cancel + + @endif + + {{ $scriptId ? 'Update Script' : 'Create Script' }} + +
+ diff --git a/resources/views/livewire/security/cloud-init-scripts.blade.php b/resources/views/livewire/security/cloud-init-scripts.blade.php new file mode 100644 index 000000000..910b87b32 --- /dev/null +++ b/resources/views/livewire/security/cloud-init-scripts.blade.php @@ -0,0 +1,57 @@ +
+ +
+

Cloud-Init Scripts

+ @can('create', App\Models\CloudInitScript::class) + + + + @endcan +
+
Manage reusable cloud-init scripts for server initialization.
+ +
+ @forelse ($scripts as $script) +
+
+
+
+
{{ $script->name }}
+
+ Created {{ $script->created_at->diffForHumans() }} +
+
+
+ +
+
Script Preview:
+
{{ Str::limit($script->script, 200) }}
+
+ +
+ @can('update', $script) + + + + @endcan + + @can('delete', $script) + + @endcan +
+
+
+ @empty +
No cloud-init scripts found. Create one to get started.
+ @endforelse +
+
diff --git a/routes/web.php b/routes/web.php index 312bfb193..fd185496d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -34,6 +34,7 @@ use App\Livewire\Project\Shared\ScheduledTask\Show as ScheduledTaskShow; use App\Livewire\Project\Show as ProjectShow; use App\Livewire\Security\ApiTokens; +use App\Livewire\Security\CloudInitScripts; use App\Livewire\Security\CloudTokens; use App\Livewire\Security\PrivateKey\Index as SecurityPrivateKeyIndex; use App\Livewire\Security\PrivateKey\Show as SecurityPrivateKeyShow; @@ -275,6 +276,7 @@ Route::get('/security/private-key/{private_key_uuid}', SecurityPrivateKeyShow::class)->name('security.private-key.show'); Route::get('/security/cloud-tokens', CloudTokens::class)->name('security.cloud-tokens'); + Route::get('/security/cloud-init-scripts', CloudInitScripts::class)->name('security.cloud-init-scripts'); Route::get('/security/api-tokens', ApiTokens::class)->name('security.api-tokens'); }); From 6c5adce633bd5cc53d1975c045f25d246aa4fccb Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 11 Oct 2025 13:30:44 +0200 Subject: [PATCH 10/20] fix: improve cloud-init scripts UI styling and behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix multiple UI/UX issues with cloud-init scripts management: 1. Fix card styling - Remove purple box background, use simple border - Changed from .box class to inline flex/border styling - Matches cloud provider tokens styling pattern 2. Remove script preview section - Preview was taking too much space and looked cluttered - Users can edit to see full script content 3. Make edit modal full width - Added fullWidth attribute to x-modal-input component - Provides better editing experience for long scripts 4. Fix fields clearing after update - Fields were being reset even in edit mode - Now only reset fields when creating new script - Edit mode preserves values after save 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Livewire/Security/CloudInitScriptForm.php | 6 +- .../security/cloud-init-scripts.blade.php | 57 ++++++++----------- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/app/Livewire/Security/CloudInitScriptForm.php b/app/Livewire/Security/CloudInitScriptForm.php index 5307e28b3..ff670cd4f 100644 --- a/app/Livewire/Security/CloudInitScriptForm.php +++ b/app/Livewire/Security/CloudInitScriptForm.php @@ -78,7 +78,11 @@ public function save() $message = 'Cloud-init script created successfully.'; } - $this->reset(['name', 'script', 'scriptId']); + // Only reset fields if creating (not editing) + if (! $this->scriptId) { + $this->reset(['name', 'script']); + } + $this->dispatch('scriptSaved'); $this->dispatch('success', $message); diff --git a/resources/views/livewire/security/cloud-init-scripts.blade.php b/resources/views/livewire/security/cloud-init-scripts.blade.php index 910b87b32..aa7324e4b 100644 --- a/resources/views/livewire/security/cloud-init-scripts.blade.php +++ b/resources/views/livewire/security/cloud-init-scripts.blade.php @@ -12,42 +12,35 @@
@forelse ($scripts as $script) -
-
-
-
-
{{ $script->name }}
-
- Created {{ $script->created_at->diffForHumans() }} -
+
+
+
+
{{ $script->name }}
+
+ Created {{ $script->created_at->diffForHumans() }}
+
-
-
Script Preview:
-
{{ Str::limit($script->script, 200) }}
-
+
+ @can('update', $script) + + + + @endcan -
- @can('update', $script) - - - - @endcan - - @can('delete', $script) - - @endcan -
+ @can('delete', $script) + + @endcan
@empty From ff69bf17cdbda3090380c6ba90ae8a32d134fdfd Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 11 Oct 2025 13:33:55 +0200 Subject: [PATCH 11/20] feat: add cloud-init scripts to global search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add cloud-init scripts to the global search navigation routes, making them discoverable via the quick search (Cmd+K / Ctrl+K). Changes: - Added dedicated "Cloud-Init Scripts" navigation entry - Searchable via: cloud-init, scripts, cloud init, cloudinit, initialization, startup, server setup - Updated Security entry to include cloud-init in search terms - Links to /security/cloud-init-scripts route Users can now quickly navigate to cloud-init script management by typing "cloud-init" or related terms in global search. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Livewire/GlobalSearch.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/Livewire/GlobalSearch.php b/app/Livewire/GlobalSearch.php index 87008e45e..680ac7701 100644 --- a/app/Livewire/GlobalSearch.php +++ b/app/Livewire/GlobalSearch.php @@ -617,7 +617,14 @@ private function loadSearchableItems() 'type' => 'navigation', 'description' => 'Manage private keys and API tokens', 'link' => route('security.private-key.index'), - 'search_text' => 'security private keys ssh api tokens', + 'search_text' => 'security private keys ssh api tokens cloud-init scripts', + ], + [ + 'name' => 'Cloud-Init Scripts', + 'type' => 'navigation', + 'description' => 'Manage reusable cloud-init scripts', + 'link' => route('security.cloud-init-scripts'), + 'search_text' => 'cloud-init scripts cloud init cloudinit initialization startup server setup', ], [ 'name' => 'Sources', From 64c4ce210e095ac50aadd3806f7b22d85a1854e2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 11 Oct 2025 13:36:14 +0200 Subject: [PATCH 12/20] feat: add artisan command to clear global search cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new artisan command for manually clearing the global search cache during development and testing. This is useful when testing new navigation entries or updates to searchable resources without waiting for the 5-minute cache TTL. Command: php artisan search:clear Usage options: - search:clear - Clear cache for current user's team - search:clear --team=1 - Clear cache for specific team ID - search:clear --all - Clear cache for all teams This helps developers test global search changes immediately, especially when adding new navigation routes like cloud-init scripts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../Commands/ClearGlobalSearchCache.php | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 app/Console/Commands/ClearGlobalSearchCache.php diff --git a/app/Console/Commands/ClearGlobalSearchCache.php b/app/Console/Commands/ClearGlobalSearchCache.php new file mode 100644 index 000000000..a368b0bad --- /dev/null +++ b/app/Console/Commands/ClearGlobalSearchCache.php @@ -0,0 +1,83 @@ +option('all')) { + return $this->clearAllTeamsCache(); + } + + if ($teamId = $this->option('team')) { + return $this->clearTeamCache($teamId); + } + + // If no options provided, clear cache for current user's team + if (! auth()->check()) { + $this->error('No authenticated user found. Use --team=ID or --all option.'); + + return Command::FAILURE; + } + + $teamId = auth()->user()->currentTeam()->id; + + return $this->clearTeamCache($teamId); + } + + private function clearTeamCache(int $teamId): int + { + $team = Team::find($teamId); + + if (! $team) { + $this->error("Team with ID {$teamId} not found."); + + return Command::FAILURE; + } + + GlobalSearch::clearTeamCache($teamId); + $this->info("✓ Cleared global search cache for team: {$team->name} (ID: {$teamId})"); + + return Command::SUCCESS; + } + + private function clearAllTeamsCache(): int + { + $teams = Team::all(); + + if ($teams->isEmpty()) { + $this->warn('No teams found.'); + + return Command::SUCCESS; + } + + $count = 0; + foreach ($teams as $team) { + GlobalSearch::clearTeamCache($team->id); + $count++; + } + + $this->info("✓ Cleared global search cache for {$count} team(s)"); + + return Command::SUCCESS; + } +} From 2ce3052378f1dd451b6e79a9179c0a9eebb1549d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 11 Oct 2025 13:43:14 +0200 Subject: [PATCH 13/20] fix: allow typing in global search while data loads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove disabled attribute from search input and defer search execution until data is loaded. Users can now start typing immediately when opening global search (Cmd+K), improving perceived responsiveness. Search results are only computed once isLoadingInitialData is false. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../views/livewire/global-search.blade.php | 280 ++++++++---------- 1 file changed, 129 insertions(+), 151 deletions(-) diff --git a/resources/views/livewire/global-search.blade.php b/resources/views/livewire/global-search.blade.php index d8cbcf2d8..3df03ea0d 100644 --- a/resources/views/livewire/global-search.blade.php +++ b/resources/views/livewire/global-search.blade.php @@ -14,6 +14,11 @@ return []; } + // Don't execute search if data is still loading + if (this.isLoadingInitialData) { + return []; + } + const query = this.searchQuery.toLowerCase().trim(); const results = this.allSearchableItems.filter(item => { @@ -28,6 +33,12 @@ if (!this.searchQuery || this.searchQuery.length < 1) { return []; } + + // Don't execute search if data is still loading + if (this.isLoadingInitialData) { + return []; + } + const query = this.searchQuery.toLowerCase().trim(); if (query === 'new') { @@ -239,7 +250,8 @@ class="fixed top-0 left-0 z-99 flex items-start justify-center w-screen h-screen pt-[10vh]">
-