diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php index 591780cfb..5176f5ff9 100644 --- a/app/Livewire/Project/Database/Dragonfly/General.php +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -57,7 +57,8 @@ public function getListeners() return [ "echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped', - "echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh', + "echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh', + "echo-private:team.{$teamId},ServiceChecked" => 'refresh', ]; } @@ -299,4 +300,10 @@ public function regenerateSslCertificate() handleError($e, $this); } } + + public function refresh(): void + { + $this->database->refresh(); + $this->syncData(); + } } diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php index 35799e55f..b50f196a8 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -59,7 +59,8 @@ public function getListeners() return [ "echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped', - "echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh', + "echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh', + "echo-private:team.{$teamId},ServiceChecked" => 'refresh', ]; } @@ -304,4 +305,10 @@ public function regenerateSslCertificate() handleError($e, $this); } } + + public function refresh(): void + { + $this->database->refresh(); + $this->syncData(); + } } diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php index 5615765fd..9a1a8bd68 100644 --- a/app/Livewire/Project/Database/Mariadb/General.php +++ b/app/Livewire/Project/Database/Mariadb/General.php @@ -61,9 +61,11 @@ class General extends Component public function getListeners() { $userId = Auth::id(); + $teamId = Auth::user()->currentTeam()->id; return [ - "echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh', + "echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh', + "echo-private:team.{$teamId},ServiceChecked" => 'refresh', ]; } diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php index 0bc6d1e2f..a21de744a 100644 --- a/app/Livewire/Project/Database/Mongodb/General.php +++ b/app/Livewire/Project/Database/Mongodb/General.php @@ -61,9 +61,11 @@ class General extends Component public function getListeners() { $userId = Auth::id(); + $teamId = Auth::user()->currentTeam()->id; return [ - "echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh', + "echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh', + "echo-private:team.{$teamId},ServiceChecked" => 'refresh', ]; } diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php index df244662e..cacb4ac49 100644 --- a/app/Livewire/Project/Database/Mysql/General.php +++ b/app/Livewire/Project/Database/Mysql/General.php @@ -63,9 +63,11 @@ class General extends Component public function getListeners() { $userId = Auth::id(); + $teamId = Auth::user()->currentTeam()->id; return [ - "echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh', + "echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh', + "echo-private:team.{$teamId},ServiceChecked" => 'refresh', ]; } diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php index f862e0cc6..22e350683 100644 --- a/app/Livewire/Project/Database/Postgresql/General.php +++ b/app/Livewire/Project/Database/Postgresql/General.php @@ -71,9 +71,11 @@ class General extends Component public function getListeners() { $userId = Auth::id(); + $teamId = Auth::user()->currentTeam()->id; return [ - "echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh', + "echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh', + "echo-private:team.{$teamId},ServiceChecked" => 'refresh', 'save_init_script', 'delete_init_script', ]; @@ -488,4 +490,10 @@ public function submit() } } } + + public function refresh(): void + { + $this->database->refresh(); + $this->syncData(); + } } diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index 2eec14c01..3c32a6192 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -59,9 +59,11 @@ class General extends Component public function getListeners() { $userId = Auth::id(); + $teamId = Auth::user()->currentTeam()->id; return [ - "echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh', + "echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh', + "echo-private:team.{$teamId},ServiceChecked" => 'refresh', 'envsUpdated' => 'refresh', ]; } diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index ceae64d84..e4feec692 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -237,10 +237,11 @@ function clone_application(Application $source, $destination, array $overrides = 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'application_id' => $newApplication->id, ]); $newApplicationSettings->save(); + $newApplication->setRelation('settings', $newApplicationSettings->fresh()); } // Clone tags @@ -256,7 +257,7 @@ function clone_application(Application $source, $destination, array $overrides = 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'uuid' => (string) new Cuid2, 'application_id' => $newApplication->id, 'team_id' => currentTeam()->id, @@ -271,7 +272,7 @@ function clone_application(Application $source, $destination, array $overrides = 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'uuid' => (string) new Cuid2, 'application_id' => $newApplication->id, 'status' => 'exited', @@ -303,7 +304,7 @@ function clone_application(Application $source, $destination, array $overrides = 'created_at', 'updated_at', 'uuid', - ])->fill([ + ])->forceFill([ 'name' => $newName, 'resource_id' => $newApplication->id, ]); @@ -339,7 +340,7 @@ function clone_application(Application $source, $destination, array $overrides = 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'resource_id' => $newApplication->id, ]); $newStorage->save(); @@ -353,7 +354,7 @@ function clone_application(Application $source, $destination, array $overrides = 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'resourceable_id' => $newApplication->id, 'resourceable_type' => $newApplication->getMorphClass(), 'is_preview' => false, @@ -370,7 +371,7 @@ function clone_application(Application $source, $destination, array $overrides = 'id', 'created_at', 'updated_at', - ])->fill([ + ])->forceFill([ 'resourceable_id' => $newApplication->id, 'resourceable_type' => $newApplication->getMorphClass(), 'is_preview' => true, diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index cd773f6a9..a43f2e340 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1919,7 +1919,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal // Create new serviceApplication or serviceDatabase if ($isDatabase) { if ($isNew) { - $savedService = ServiceDatabase::create([ + $savedService = ServiceDatabase::forceCreate([ 'name' => $serviceName, 'image' => $image, 'service_id' => $resource->id, @@ -1930,7 +1930,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'service_id' => $resource->id, ])->first(); if (is_null($savedService)) { - $savedService = ServiceDatabase::create([ + $savedService = ServiceDatabase::forceCreate([ 'name' => $serviceName, 'image' => $image, 'service_id' => $resource->id, @@ -1939,7 +1939,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } } else { if ($isNew) { - $savedService = ServiceApplication::create([ + $savedService = ServiceApplication::forceCreate([ 'name' => $serviceName, 'image' => $image, 'service_id' => $resource->id, @@ -1950,7 +1950,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'service_id' => $resource->id, ])->first(); if (is_null($savedService)) { - $savedService = ServiceApplication::create([ + $savedService = ServiceApplication::forceCreate([ 'name' => $serviceName, 'image' => $image, 'service_id' => $resource->id, diff --git a/openapi.json b/openapi.json index ed8decb48..239068300 100644 --- a/openapi.json +++ b/openapi.json @@ -4331,6 +4331,11 @@ "database_backup_retention_max_storage_s3": { "type": "integer", "description": "Max storage (MB) for S3 backups" + }, + "timeout": { + "type": "integer", + "description": "Backup job timeout in seconds (min: 60, max: 36000)", + "default": 3600 } }, "type": "object" @@ -4896,6 +4901,11 @@ "database_backup_retention_max_storage_s3": { "type": "integer", "description": "Max storage of the backup in S3" + }, + "timeout": { + "type": "integer", + "description": "Backup job timeout in seconds (min: 60, max: 36000)", + "default": 3600 } }, "type": "object" @@ -10451,6 +10461,26 @@ "none" ], "description": "The proxy type." + }, + "concurrent_builds": { + "type": "integer", + "description": "Number of concurrent builds." + }, + "dynamic_timeout": { + "type": "integer", + "description": "Deployment timeout in seconds." + }, + "deployment_queue_limit": { + "type": "integer", + "description": "Maximum number of queued deployments." + }, + "server_disk_usage_notification_threshold": { + "type": "integer", + "description": "Server disk usage notification threshold (%)." + }, + "server_disk_usage_check_frequency": { + "type": "string", + "description": "Cron expression for disk usage check frequency." } }, "type": "object" diff --git a/openapi.yaml b/openapi.yaml index 157cd9f69..5bf6059af 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2734,6 +2734,10 @@ paths: database_backup_retention_max_storage_s3: type: integer description: 'Max storage (MB) for S3 backups' + timeout: + type: integer + description: 'Backup job timeout in seconds (min: 60, max: 36000)' + default: 3600 type: object responses: '201': @@ -3125,6 +3129,10 @@ paths: database_backup_retention_max_storage_s3: type: integer description: 'Max storage of the backup in S3' + timeout: + type: integer + description: 'Backup job timeout in seconds (min: 60, max: 36000)' + default: 3600 type: object responses: '200': @@ -6669,6 +6677,21 @@ paths: type: string enum: [traefik, caddy, none] description: 'The proxy type.' + concurrent_builds: + type: integer + description: 'Number of concurrent builds.' + dynamic_timeout: + type: integer + description: 'Deployment timeout in seconds.' + deployment_queue_limit: + type: integer + description: 'Maximum number of queued deployments.' + server_disk_usage_notification_threshold: + type: integer + description: 'Server disk usage notification threshold (%).' + server_disk_usage_check_frequency: + type: string + description: 'Cron expression for disk usage check frequency.' type: object responses: '201': diff --git a/resources/css/app.css b/resources/css/app.css index 3cfa03dae..2c30baf64 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -14,7 +14,9 @@ @custom-variant dark (&:where(.dark, .dark *)); @theme { - --font-sans: Inter, sans-serif; + --font-sans: 'Geist Sans', Inter, sans-serif; + --font-geist-sans: 'Geist Sans', Inter, sans-serif; + --font-logs: 'Geist Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; --color-base: #101010; --color-warning: #fcd452; @@ -96,7 +98,7 @@ body { } body { - @apply min-h-screen text-sm antialiased scrollbar overflow-x-hidden; + @apply min-h-screen text-sm font-sans antialiased scrollbar overflow-x-hidden; } .coolify-monaco-editor { diff --git a/resources/css/fonts.css b/resources/css/fonts.css index c8c4448eb..e5c6a694d 100644 --- a/resources/css/fonts.css +++ b/resources/css/fonts.css @@ -70,3 +70,18 @@ @font-face { src: url('../fonts/inter-v13-cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese-regular.woff2') format('woff2'); } +@font-face { + font-display: swap; + font-family: 'Geist Mono'; + font-style: normal; + font-weight: 100 900; + src: url('../fonts/geist-mono-variable.woff2') format('woff2'); +} + +@font-face { + font-display: swap; + font-family: 'Geist Sans'; + font-style: normal; + font-weight: 100 900; + src: url('../fonts/geist-sans-variable.woff2') format('woff2'); +} diff --git a/resources/fonts/geist-mono-variable.woff2 b/resources/fonts/geist-mono-variable.woff2 new file mode 100644 index 000000000..c8a7d8401 Binary files /dev/null and b/resources/fonts/geist-mono-variable.woff2 differ diff --git a/resources/fonts/geist-sans-variable.woff2 b/resources/fonts/geist-sans-variable.woff2 new file mode 100644 index 000000000..7ebce69dc Binary files /dev/null and b/resources/fonts/geist-sans-variable.woff2 differ diff --git a/resources/js/terminal.js b/resources/js/terminal.js index 3c52edfa0..aa5f37353 100644 --- a/resources/js/terminal.js +++ b/resources/js/terminal.js @@ -186,7 +186,7 @@ export function initializeTerminalComponent() { this.term = new Terminal({ cols: 80, rows: 30, - fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"', + fontFamily: '"Geist Mono", "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", monospace, "Powerline Extra Symbols"', cursorBlink: true, rendererType: 'canvas', convertEol: true, diff --git a/resources/views/livewire/activity-monitor.blade.php b/resources/views/livewire/activity-monitor.blade.php index 290a91857..72b68edd0 100644 --- a/resources/views/livewire/activity-monitor.blade.php +++ b/resources/views/livewire/activity-monitor.blade.php @@ -52,7 +52,7 @@ 'flex-1 min-h-0' => $fullHeight, 'max-h-96' => !$fullHeight, ])> -
{{ RunRemoteProcess::decodeOutput($activity) }}
+
{{ RunRemoteProcess::decodeOutput($activity) }}
@else @if ($showWaiting) diff --git a/resources/views/livewire/project/application/deployment/show.blade.php b/resources/views/livewire/project/application/deployment/show.blade.php index 28872f4bc..c17cda55f 100644 --- a/resources/views/livewire/project/application/deployment/show.blade.php +++ b/resources/views/livewire/project/application/deployment/show.blade.php @@ -330,7 +330,7 @@ class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-
-
+
No matches found. @@ -356,7 +356,7 @@ class="shrink-0 text-gray-500">{{ $line['timestamp'] }} ])>{{ $lineContent }}
@empty - No logs yet. + No logs yet. @endforelse
diff --git a/resources/views/livewire/project/shared/get-logs.blade.php b/resources/views/livewire/project/shared/get-logs.blade.php index ee5b65cf5..cb2dcfed1 100644 --- a/resources/views/livewire/project/shared/get-logs.blade.php +++ b/resources/views/livewire/project/shared/get-logs.blade.php @@ -480,7 +480,7 @@ class="flex overflow-y-auto overflow-x-hidden flex-col px-4 py-2 w-full min-w-0 @php $displayLines = collect(explode("\n", $outputs))->filter(fn($line) => trim($line) !== ''); @endphp -
+
No matches found. @@ -518,7 +518,7 @@ class="text-gray-500 dark:text-gray-400 py-2">
@else
No logs yet.
+ class="font-logs whitespace-pre-wrap break-all max-w-full text-neutral-400">No logs yet. @endif
diff --git a/resources/views/livewire/server/docker-cleanup-executions.blade.php b/resources/views/livewire/server/docker-cleanup-executions.blade.php index c59d53d26..d0b848cf1 100644 --- a/resources/views/livewire/server/docker-cleanup-executions.blade.php +++ b/resources/views/livewire/server/docker-cleanup-executions.blade.php @@ -100,7 +100,7 @@ - {{ data_get($result, 'command') }} + {{ data_get($result, 'command') }}
@php $output = data_get($result, 'output'); @@ -108,7 +108,7 @@ @endphp
@if($hasOutput) -
{{ $output }}
+
{{ $output }}
@else

No output returned - command completed successfully diff --git a/tests/Feature/ClonePersistentVolumeUuidTest.php b/tests/Feature/ClonePersistentVolumeUuidTest.php index f1ae8dd26..3f99c5585 100644 --- a/tests/Feature/ClonePersistentVolumeUuidTest.php +++ b/tests/Feature/ClonePersistentVolumeUuidTest.php @@ -1,15 +1,18 @@ user = User::factory()->create(); @@ -17,7 +20,7 @@ $this->user->teams()->attach($this->team, ['role' => 'owner']); $this->server = Server::factory()->create(['team_id' => $this->team->id]); - $this->destination = StandaloneDocker::factory()->create(['server_id' => $this->server->id]); + $this->destination = $this->server->standaloneDockers()->firstOrFail(); $this->project = Project::factory()->create(['team_id' => $this->team->id]); $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); @@ -25,8 +28,13 @@ 'environment_id' => $this->environment->id, 'destination_id' => $this->destination->id, 'destination_type' => $this->destination->getMorphClass(), + 'redirect' => 'both', ]); + $this->application->settings->forceFill([ + 'is_container_label_readonly_enabled' => false, + ])->save(); + $this->actingAs($this->user); session(['currentTeam' => $this->team]); }); @@ -82,3 +90,71 @@ expect($clonedUuids)->each->not->toBeIn($originalUuids); expect(array_unique($clonedUuids))->toHaveCount(2); }); + +test('cloning application reassigns settings to the cloned application', function () { + $this->application->settings->forceFill([ + 'is_static' => true, + 'is_spa' => true, + 'is_build_server_enabled' => true, + ])->save(); + + $newApp = clone_application($this->application, $this->destination, [ + 'environment_id' => $this->environment->id, + ]); + + $sourceSettingsCount = ApplicationSetting::query() + ->where('application_id', $this->application->id) + ->count(); + $clonedSettings = ApplicationSetting::query() + ->where('application_id', $newApp->id) + ->first(); + + expect($sourceSettingsCount)->toBe(1) + ->and($clonedSettings)->not->toBeNull() + ->and($clonedSettings?->application_id)->toBe($newApp->id) + ->and($clonedSettings?->is_static)->toBeTrue() + ->and($clonedSettings?->is_spa)->toBeTrue() + ->and($clonedSettings?->is_build_server_enabled)->toBeTrue(); +}); + +test('cloning application reassigns scheduled tasks and previews to the cloned application', function () { + $scheduledTask = ScheduledTask::forceCreate([ + 'uuid' => 'scheduled-task-original', + 'application_id' => $this->application->id, + 'team_id' => $this->team->id, + 'name' => 'nightly-task', + 'command' => 'php artisan schedule:run', + 'frequency' => '* * * * *', + 'container' => 'app', + 'timeout' => 120, + ]); + + $preview = ApplicationPreview::forceCreate([ + 'uuid' => 'preview-original', + 'application_id' => $this->application->id, + 'pull_request_id' => 123, + 'pull_request_html_url' => 'https://example.com/pull/123', + 'fqdn' => 'https://preview.example.com', + 'status' => 'running', + ]); + + $newApp = clone_application($this->application, $this->destination, [ + 'environment_id' => $this->environment->id, + ]); + + $clonedTask = ScheduledTask::query() + ->where('application_id', $newApp->id) + ->first(); + $clonedPreview = ApplicationPreview::query() + ->where('application_id', $newApp->id) + ->first(); + + expect($clonedTask)->not->toBeNull() + ->and($clonedTask?->uuid)->not->toBe($scheduledTask->uuid) + ->and($clonedTask?->application_id)->toBe($newApp->id) + ->and($clonedTask?->team_id)->toBe($this->team->id) + ->and($clonedPreview)->not->toBeNull() + ->and($clonedPreview?->uuid)->not->toBe($preview->uuid) + ->and($clonedPreview?->application_id)->toBe($newApp->id) + ->and($clonedPreview?->status)->toBe('exited'); +}); diff --git a/tests/Feature/DatabaseSslStatusRefreshTest.php b/tests/Feature/DatabaseSslStatusRefreshTest.php new file mode 100644 index 000000000..eab2b08db --- /dev/null +++ b/tests/Feature/DatabaseSslStatusRefreshTest.php @@ -0,0 +1,77 @@ +team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + + $this->actingAs($this->user); + session(['currentTeam' => $this->team]); +}); + +dataset('ssl-aware-database-general-components', [ + MysqlGeneral::class, + MariadbGeneral::class, + MongodbGeneral::class, + RedisGeneral::class, + PostgresqlGeneral::class, + KeydbGeneral::class, + DragonflyGeneral::class, +]); + +it('maps database status broadcasts to refresh for ssl-aware database general components', function (string $componentClass) { + $component = app($componentClass); + $listeners = $component->getListeners(); + + expect($listeners["echo-private:user.{$this->user->id},DatabaseStatusChanged"])->toBe('refresh') + ->and($listeners["echo-private:team.{$this->team->id},ServiceChecked"])->toBe('refresh'); +})->with('ssl-aware-database-general-components'); + +it('reloads the mysql database model when refreshing so ssl controls follow the latest status', function () { + $server = Server::factory()->create(['team_id' => $this->team->id]); + $destination = StandaloneDocker::where('server_id', $server->id)->first(); + $project = Project::factory()->create(['team_id' => $this->team->id]); + $environment = Environment::factory()->create(['project_id' => $project->id]); + + $database = StandaloneMysql::forceCreate([ + 'name' => 'test-mysql', + 'image' => 'mysql:8', + 'mysql_root_password' => 'password', + 'mysql_user' => 'coolify', + 'mysql_password' => 'password', + 'mysql_database' => 'coolify', + 'status' => 'exited:unhealthy', + 'enable_ssl' => true, + 'is_log_drain_enabled' => false, + 'environment_id' => $environment->id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + ]); + + $component = Livewire::test(MysqlGeneral::class, ['database' => $database]) + ->assertDontSee('Database should be stopped to change this settings.'); + + $database->forceFill(['status' => 'running:healthy'])->save(); + + $component->call('refresh') + ->assertSee('Database should be stopped to change this settings.'); +}); diff --git a/tests/Feature/LogFontStylingTest.php b/tests/Feature/LogFontStylingTest.php new file mode 100644 index 000000000..c7903fb45 --- /dev/null +++ b/tests/Feature/LogFontStylingTest.php @@ -0,0 +1,45 @@ +toContain("font-family: 'Geist Mono'") + ->toContain("url('../fonts/geist-mono-variable.woff2')") + ->toContain("font-family: 'Geist Sans'") + ->toContain("url('../fonts/geist-sans-variable.woff2')") + ->and($appCss) + ->toContain("--font-sans: 'Geist Sans', Inter, sans-serif") + ->toContain('@apply min-h-screen text-sm font-sans antialiased scrollbar overflow-x-hidden;') + ->toContain("--font-logs: 'Geist Mono'") + ->toContain("--font-geist-sans: 'Geist Sans'") + ->and($fontPath) + ->toBeFile() + ->and($geistSansPath) + ->toBeFile(); +}); + +it('uses geist mono for shared logs and terminal rendering', function () { + $sharedLogsView = file_get_contents(resource_path('views/livewire/project/shared/get-logs.blade.php')); + $deploymentLogsView = file_get_contents(resource_path('views/livewire/project/application/deployment/show.blade.php')); + $activityMonitorView = file_get_contents(resource_path('views/livewire/activity-monitor.blade.php')); + $dockerCleanupView = file_get_contents(resource_path('views/livewire/server/docker-cleanup-executions.blade.php')); + $terminalClient = file_get_contents(resource_path('js/terminal.js')); + + expect($sharedLogsView) + ->toContain('class="font-logs max-w-full cursor-default"') + ->toContain('class="font-logs whitespace-pre-wrap break-all max-w-full text-neutral-400"') + ->and($deploymentLogsView) + ->toContain('class="flex flex-col font-logs"') + ->toContain('class="font-logs text-neutral-400 mb-2"') + ->and($activityMonitorView) + ->toContain('

and($dockerCleanupView)
+        ->toContain('class="flex-1 text-sm font-logs text-gray-700 dark:text-gray-300"')
+        ->toContain('class="font-logs text-sm text-gray-600 dark:text-gray-300 whitespace-pre-wrap"')
+        ->and($terminalClient)
+        ->toContain('"Geist Mono"');
+});
diff --git a/tests/Unit/ServiceParserImageUpdateTest.php b/tests/Unit/ServiceParserImageUpdateTest.php
index 526505098..649795866 100644
--- a/tests/Unit/ServiceParserImageUpdateTest.php
+++ b/tests/Unit/ServiceParserImageUpdateTest.php
@@ -41,7 +41,8 @@
     // The new code checks for null within the else block and creates only if needed
     expect($sharedFile)
         ->toContain('if (is_null($savedService)) {')
-        ->toContain('$savedService = ServiceDatabase::create([');
+        ->toContain('$savedService = ServiceDatabase::forceCreate([')
+        ->toContain('$savedService = ServiceApplication::forceCreate([');
 });
 
 it('verifies image update logic is present in parseDockerComposeFile', function () {