diff --git a/.cursor/rules/security-patterns.mdc b/.cursor/rules/security-patterns.mdc index d47d03375..9cdbcaa0c 100644 --- a/.cursor/rules/security-patterns.mdc +++ b/.cursor/rules/security-patterns.mdc @@ -21,7 +21,9 @@ Coolify implements **defense-in-depth security** with multiple layers of protect - **Supported Providers**: - Google OAuth - Microsoft Azure AD + - Clerk - Authentik + - Discord - GitHub (via GitHub Apps) - GitLab diff --git a/.cursor/rules/technology-stack.mdc b/.cursor/rules/technology-stack.mdc index 1e894720c..81a2e3bb3 100644 --- a/.cursor/rules/technology-stack.mdc +++ b/.cursor/rules/technology-stack.mdc @@ -90,7 +90,7 @@ alwaysApply: false - **Purpose**: OAuth provider integration - **Providers**: - GitHub, GitLab, Google - - Microsoft Azure, Authentik + - Microsoft Azure, Authentik, Discord, Clerk - Custom OAuth implementations ## Background Processing diff --git a/CHANGELOG.md b/CHANGELOG.md index a3576438e..1bf445f74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,190 @@ # Changelog All notable changes to this project will be documented in this file. -## [4.0.0-beta.419] - 2025-06-16 +## [unreleased] + +### 📚 Documentation + +- Update changelog + +### ⚙️ Miscellaneous Tasks + +- *(bump)* Update composer deps +- *(version)* Bump Coolify version to 4.0.0-beta.420.6 + +## [4.0.0-beta.420.5] - 2025-07-08 + +### 🚀 Features + +- *(scheduling)* Add command to manually run scheduled database backups and tasks with options for chunking, delays, and dry runs + +### 🐛 Bug Fixes + +- *(versions)* Update coolify version numbers in versions.json and constants.php to 4.0.0-beta.420.5 and 4.0.0-beta.420.6 +- *(database)* Ensure internal port defaults correctly for unsupported database types in StartDatabaseProxy + +### 🚜 Refactor + +- *(postgresql)* Improve layout and spacing in SSL and Proxy configuration sections for better UI consistency + +## [4.0.0-beta.420.4] - 2025-07-08 + +### 🐛 Bug Fixes + +- *(service)* Update Postiz compose configuration for improved server availability +- *(install.sh)* Use IPV4_PUBLIC_IP variable in output instead of repeated curl +- *(env)* Generate literal env variables better +- *(deployment)* Update x-data initialization in deployment view for improved functionality +- *(deployment)* Enhance COOLIFY_URL and COOLIFY_FQDN variable generation for better compatibility +- *(deployment)* Improve docker-compose domain handling and environment variable generation +- *(deployment)* Refactor domain parsing and environment variable generation using Spatie URL library +- *(deployment)* Update COOLIFY_URL and COOLIFY_FQDN generation to use Spatie URL library for improved accuracy +- *(scheduling)* Change redis cleanup command frequency from hourly to weekly for better resource management + +### 🚜 Refactor + +- *(previews)* Streamline preview URL generation by utilizing application method +- *(application)* Adjust layout and spacing in general application view for improved UI + +### 📚 Documentation + +- Update changelog +- Update changelog + +## [4.0.0-beta.420.3] - 2025-07-03 + +### 📚 Documentation + +- Update changelog + +## [4.0.0-beta.420.2] - 2025-07-03 + +### 🚀 Features + +- *(template)* Added excalidraw (#6095) +- *(template)* Add excalidraw service configuration with documentation and tags + +### 🐛 Bug Fixes + +- *(terminal)* Ensure shell execution only uses valid shell if available in terminal command +- *(ui)* Improve destination selection description for clarity in resource segregation +- *(jobs)* Update middleware to use expireAfter for WithoutOverlapping in multiple job classes +- Removing eager loading (#6071) +- *(template)* Adjust health check interval and retries for excalidraw service +- *(ui)* Env variable settings wrong order +- *(service)* Ensure configuration changes are properly tracked and dispatched + +### 🚜 Refactor + +- *(ui)* Enhance project cloning interface with improved table layout for server and resource selection +- *(terminal)* Simplify command construction for SSH execution +- *(settings)* Streamline instance admin checks and initialization of settings in Livewire components +- *(policy)* Optimize team membership checks in S3StoragePolicy +- *(popup)* Improve styling and structure of the small popup component +- *(shared)* Enhance FQDN generation logic for services in newParser function +- *(redis)* Enhance CleanupRedis command with dry-run option and improved key deletion logic +- *(init)* Standardize method naming conventions and improve command structure in Init.php +- *(shared)* Improve error handling in getTopLevelNetworks function to return network name on invalid docker-compose.yml +- *(database)* Improve error handling for unsupported database types in StartDatabaseProxy + +### 📚 Documentation + +- Update changelog + +### ⚙️ Miscellaneous Tasks + +- *(versions)* Bump coolify and nightly versions to 4.0.0-beta.420.3 and 4.0.0-beta.420.4 respectively +- *(versions)* Update coolify and nightly versions to 4.0.0-beta.420.4 and 4.0.0-beta.420.5 respectively + +## [4.0.0-beta.420.1] - 2025-06-26 + +### 🐛 Bug Fixes + +- *(server)* Prepend 'mux_' to UUID in muxFilename method for consistent naming +- *(ui)* Enhance terminal access messaging to clarify server functionality and terminal status +- *(database)* Proxy ssl port if ssl is enabled + +### 🚜 Refactor + +- *(ui)* Separate views for instance settings to separate paths to make it cleaner +- *(ui)* Remove unnecessary step3ButtonText attributes from modal confirmation components for cleaner code + +### 📚 Documentation + +- Update changelog + +### ⚙️ Miscellaneous Tasks + +- *(versions)* Update Coolify versions to 4.0.0-beta.420.2 and 4.0.0-beta.420.3 in multiple files + +## [4.0.0-beta.420] - 2025-06-26 + +### 🚀 Features + +- *(service)* Add Miniflux service (#5843) +- *(service)* Add Pingvin Share service (#5969) +- *(auth)* Add Discord OAuth Provider (#5552) +- *(auth)* Add Clerk OAuth Provider (#5553) +- *(auth)* Add Zitadel OAuth Provider (#5490) +- *(core)* Set custom API rate limit (#5984) +- *(service)* Enhance service status handling and UI updates +- *(cleanup)* Add functionality to delete teams with no members or servers in CleanupStuckedResources command +- *(ui)* Add heart icon and enhance popup messaging for sponsorship support +- *(settings)* Add sponsorship popup toggle and corresponding database migration +- *(migrations)* Add optimized indexes to activity_log for improved query performance + +### 🐛 Bug Fixes + +- *(service)* Audiobookshelf healthcheck command (#5993) +- *(service)* Downgrade Evolution API phone version (#5977) +- *(service)* Pingvinshare-with-clamav +- *(ssh)* Scp requires square brackets for ipv6 (#6001) +- *(github)* Changing github app breaks the webhook. it does not anymore +- *(parser)* Improve FQDN generation and update environment variable handling +- *(ui)* Enhance status refresh buttons with loading indicators +- *(ui)* Update confirmation button text for stopping database and service +- *(routes)* Update middleware for deploy route to use 'api.ability:deploy' +- *(ui)* Refine API token creation form and update helper text for clarity +- *(ui)* Adjust layout of deployments section for improved alignment +- *(ui)* Adjust project grid layout and refine server border styling for better visibility +- *(ui)* Update border styling for consistency across components and enhance loading indicators +- *(ui)* Add padding to section headers in settings views for improved spacing +- *(ui)* Reduce gap between input fields in email settings for better alignment +- *(docker)* Conditionally enable gzip compression in Traefik labels based on configuration +- *(parser)* Enable gzip compression conditionally for Pocketbase images and streamline service creation logic +- *(ui)* Update padding for trademarks policy and enhance spacing in advanced settings section +- *(ui)* Correct closing tag for sponsorship link in layout popups +- *(ui)* Refine wording in sponsorship donation prompt in layout popups +- *(ui)* Update navbar icon color and enhance popup layout for sponsorship support +- *(ui)* Add target="_blank" to sponsorship links in layout popups for improved user experience +- *(models)* Refine comment wording in User model for clarity on user deletion criteria +- *(models)* Improve user deletion logic in User model to handle team member roles and prevent deletion if user is alone in root team +- *(ui)* Update wording in sponsorship prompt for clarity and engagement +- *(shared)* Refactor gzip handling for Pocketbase in newParser function for improved clarity + +### 🚜 Refactor + +- *(service)* Update Hoarder to their new name karakeep (#5964) +- *(service)* Karakeep naming and formatting +- *(service)* Improve miniflux +- *(core)* Rename API rate limit ENV +- *(ui)* Simplify container selection form in execute-container-command view +- *(email)* Streamline SMTP and resend settings logic for improved clarity +- *(invitation)* Rename methods for consistency and enhance invitation deletion logic +- *(user)* Streamline user deletion process and enhance team management logic + +### 📚 Documentation + +- Update changelog + +### ⚙️ Miscellaneous Tasks + +- *(service)* Update Evolution API image to the official one (#6031) +- *(versions)* Bump coolify versions to v4.0.0-beta.420 and v4.0.0-beta.421 +- *(dependencies)* Update composer dependencies to latest versions including resend-laravel to ^0.19.0 and aws-sdk-php to 3.347.0 +- *(versions)* Update Coolify version to 4.0.0-beta.420.1 and add new services (karakeep, miniflux, pingvinshare) to service templates + +## [4.0.0-beta.419] - 2025-06-17 ### 🚀 Features @@ -50,6 +233,11 @@ ### 🚀 Features - *(proxy-dashboard)* Implement ProxyDashboardCacheService to manage Traefik dashboard cache; clear cache on configuration changes and proxy actions - *(terminal-connection)* Enhance terminal connection handling with auto-connect feature and improved status messaging - *(terminal)* Implement resize handling with ResizeObserver for improved terminal responsiveness +- *(migration)* Add is_sentinel_enabled column to server_settings with default true +- *(seeder)* Dispatch StartProxy action for each server in ProductionSeeder +- *(seeder)* Add CheckAndStartSentinelJob dispatch for each server in ProductionSeeder +- *(seeder)* Conditionally dispatch StartProxy action based on proxy check result +- *(service)* Update Changedetection template (#5937) ### 🐛 Bug Fixes @@ -121,6 +309,11 @@ ### 🐛 Bug Fixes - *(terminal)* Now it should work - *(degraded-status)* Remove unnecessary whitespace in badge element for cleaner HTML - *(routes)* Add name to security route for improved route management +- *(migration)* Update default value handling for is_sentinel_enabled column in server_settings +- *(seeder)* Conditionally dispatch CheckAndStartSentinelJob based on server's sentinel status +- *(service)* Disable healthcheck logging for Gotenberg (#6005) +- *(service)* Joplin volume name (#5930) +- *(server)* Update sentinelUpdatedAt assignment to use server's sentinel_updated_at property ### 💼 Other @@ -198,6 +391,9 @@ ### 🚜 Refactor - *(navigation)* Remove wire:navigate directive from configuration links for cleaner HTML structure - *(proxy)* Update StartProxy calls to use named parameter for async option - *(clone-project)* Enhance server retrieval by including destinations and filtering out build servers +- *(ui)* Terminal +- *(ui)* Remove terminal header from execute-container-command view +- *(ui)* Remove unnecessary padding from deployment, backup, and logs sections ### 📚 Documentation @@ -205,6 +401,7 @@ ### 📚 Documentation - *(service)* Add new docs link for zipline (#5912) - Update changelog - Update changelog +- Update changelog ### 🎨 Styling @@ -236,6 +433,9 @@ ### ⚙️ Miscellaneous Tasks - *(api)* Update API docs - *(dependencies)* Update package versions in composer.json and composer.lock for improved compatibility and performance - *(dependencies)* Update package versions in package.json and package-lock.json for improved stability and features +- *(version)* Update coolify-realtime to version 1.0.9 in docker-compose and versions files +- *(version)* Update coolify version to 4.0.0-beta.420 and nightly version to 4.0.0-beta.421 +- *(service)* Changedetection remove unused code ## [4.0.0-beta.417] - 2025-05-07 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..a3bb31cee --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,87 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Coolify is an open-source, self-hostable platform for deploying applications and managing servers - an alternative to Heroku/Netlify/Vercel. It's built with Laravel (PHP) and uses Docker for containerization. + +## Development Commands + +### Frontend Development +- `npm run dev` - Start Vite development server for frontend assets +- `npm run build` - Build frontend assets for production + +### Backend Development +- `php artisan serve` - Start Laravel development server +- `php artisan migrate` - Run database migrations +- `php artisan queue:work` - Start queue worker for background jobs +- `php artisan horizon` - Start Laravel Horizon for queue monitoring +- `php artisan tinker` - Start interactive PHP REPL + +### Code Quality +- `./vendor/bin/pint` - Run Laravel Pint for code formatting +- `./vendor/bin/phpstan` - Run PHPStan for static analysis +- `./vendor/bin/pest` - Run Pest tests + +## Architecture Overview + +### Technology Stack +- **Backend**: Laravel 12 (PHP 8.4) +- **Frontend**: Livewire + Alpine.js + Tailwind CSS +- **Database**: PostgreSQL 15 +- **Cache/Queue**: Redis 7 +- **Real-time**: Soketi (WebSocket server) +- **Containerization**: Docker & Docker Compose + +### Key Components + +#### Core Models +- `Application` - Deployed applications with Git integration +- `Server` - Remote servers managed by Coolify +- `Service` - Docker Compose services +- `Database` - Standalone database instances (PostgreSQL, MySQL, MongoDB, Redis, etc.) +- `Team` - Multi-tenancy support +- `Project` - Grouping of environments and resources + +#### Job System +- Uses Laravel Horizon for queue management +- Key jobs: `ApplicationDeploymentJob`, `ServerCheckJob`, `DatabaseBackupJob` +- `ScheduledJobManager` and `ServerResourceManager` handle job scheduling + +#### Deployment Flow +1. Git webhook triggers deployment +2. `ApplicationDeploymentJob` handles build and deployment +3. Docker containers are managed on target servers +4. Proxy configuration (Nginx/Traefik) is updated + +#### Server Management +- SSH-based server communication via `ExecuteRemoteCommand` trait +- Docker installation and management +- Proxy configuration generation +- Resource monitoring and cleanup + +### Directory Structure +- `app/Actions/` - Domain-specific actions (Application, Database, Server, etc.) +- `app/Jobs/` - Background queue jobs +- `app/Livewire/` - Frontend components (full-stack with Livewire) +- `app/Models/` - Eloquent models +- `bootstrap/helpers/` - Helper functions for various domains +- `database/migrations/` - Database schema evolution + +## Development Guidelines + +### Code Organization +- Use Actions pattern for complex business logic +- Livewire components handle UI and user interactions +- Jobs handle asynchronous operations +- Traits provide shared functionality (e.g., `ExecuteRemoteCommand`) + +### Testing +- Uses Pest for testing framework +- Tests located in `tests/` directory + +### Deployment and Docker +- Applications are deployed using Docker containers +- Configuration generated dynamically based on application settings +- Supports multiple deployment targets and proxy configurations \ No newline at end of file diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php index 0ca703fce..ee3398b04 100644 --- a/app/Actions/Application/StopApplication.php +++ b/app/Actions/Application/StopApplication.php @@ -49,7 +49,7 @@ public function handle(Application $application, bool $previewDeployments = fals } if ($dockerCleanup) { - CleanupDocker::dispatch($server, true); + CleanupDocker::dispatch($server, false, false); } } catch (\Exception $e) { return $e->getMessage(); diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php index 744bbaa50..12fd92792 100644 --- a/app/Actions/Database/StartDatabaseProxy.php +++ b/app/Actions/Database/StartDatabaseProxy.php @@ -27,6 +27,8 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St $server = data_get($database, 'destination.server'); $containerName = data_get($database, 'uuid'); $proxyContainerName = "{$database->uuid}-proxy"; + $isSSLEnabled = $database->enable_ssl ?? false; + if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) { $databaseType = $database->databaseType(); $network = $database->service->uuid; @@ -42,6 +44,12 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St 'standalone-mongodb' => 27017, default => throw new \Exception("Unsupported database type: $databaseType"), }; + if ($isSSLEnabled) { + $internalPort = match ($databaseType) { + 'standalone-redis', 'standalone-keydb', 'standalone-dragonfly' => 6380, + default => $internalPort, + }; + } $configuration_dir = database_proxy_dir($database->uuid); if (isDev()) { diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php index a03c9269e..5c881e743 100644 --- a/app/Actions/Database/StopDatabase.php +++ b/app/Actions/Database/StopDatabase.php @@ -18,7 +18,7 @@ class StopDatabase { use AsAction; - public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database, bool $isDeleteOperation = false, bool $dockerCleanup = true) + public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database, bool $dockerCleanup = true) { try { $server = $database->destination->server; @@ -29,7 +29,7 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St $this->stopContainer($database, $database->uuid, 30); if ($dockerCleanup) { - CleanupDocker::dispatch($server, true); + CleanupDocker::dispatch($server, false, false); } if ($database->is_public) { diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php index d4b03ffc1..a06e547c5 100644 --- a/app/Actions/Proxy/CheckProxy.php +++ b/app/Actions/Proxy/CheckProxy.php @@ -66,7 +66,7 @@ public function handle(Server $server, $fromUI = false): bool if ($server->id === 0) { $ip = 'host.docker.internal'; } - $portsToCheck = ['80', '443']; + $portsToCheck = []; try { if ($server->proxyType() !== ProxyTypes::NONE->value) { diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php index 754feecb1..392562167 100644 --- a/app/Actions/Server/CleanupDocker.php +++ b/app/Actions/Server/CleanupDocker.php @@ -11,7 +11,7 @@ class CleanupDocker public string $jobQueue = 'high'; - public function handle(Server $server) + public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $deleteUnusedNetworks = false) { $settings = instanceSettings(); $realtimeImage = config('constants.coolify.realtime_image'); @@ -36,11 +36,11 @@ public function handle(Server $server) "docker images --filter before=$realtimeImageWithoutPrefixVersion --filter reference=$realtimeImageWithoutPrefix | grep $realtimeImageWithoutPrefix | awk '{print $3}' | xargs -r docker rmi -f", ]; - if ($server->settings->delete_unused_volumes) { + if ($deleteUnusedVolumes) { $commands[] = 'docker volume prune -af'; } - if ($server->settings->delete_unused_networks) { + if ($deleteUnusedNetworks) { $commands[] = 'docker network prune -f'; } diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php index 9a6cc140b..2a06428e2 100644 --- a/app/Actions/Server/UpdateCoolify.php +++ b/app/Actions/Server/UpdateCoolify.php @@ -29,7 +29,7 @@ public function handle($manual_update = false) if (! $this->server) { return; } - CleanupDocker::dispatch($this->server); + CleanupDocker::dispatch($this->server, false, false); $this->latestVersion = get_latest_version_of_coolify(); $this->currentVersion = config('constants.coolify.version'); if (! $manual_update) { diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php index 404e11559..8790901cd 100644 --- a/app/Actions/Service/DeleteService.php +++ b/app/Actions/Service/DeleteService.php @@ -11,7 +11,7 @@ class DeleteService { use AsAction; - public function handle(Service $service, bool $deleteConfigurations, bool $deleteVolumes, bool $dockerCleanup, bool $deleteConnectedNetworks) + public function handle(Service $service, bool $deleteVolumes, bool $deleteConnectedNetworks, bool $deleteConfigurations, bool $dockerCleanup) { try { $server = data_get($service, 'server'); @@ -71,7 +71,7 @@ public function handle(Service $service, bool $deleteConfigurations, bool $delet $service->forceDelete(); if ($dockerCleanup) { - CleanupDocker::dispatch($server, true); + CleanupDocker::dispatch($server, false, false); } } } diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php index 1e7779a75..dfef6a566 100644 --- a/app/Actions/Service/StartService.php +++ b/app/Actions/Service/StartService.php @@ -19,6 +19,7 @@ public function handle(Service $service, bool $pullLatestImages = false, bool $s StopService::run(service: $service, dockerCleanup: false); } $service->saveComposeConfigs(); + $service->isConfigurationChanged(save: true); $commands[] = 'cd '.$service->workdir(); $commands[] = "echo 'Saved configuration files to {$service->workdir()}.'"; if ($pullLatestImages) { @@ -41,6 +42,6 @@ public function handle(Service $service, bool $pullLatestImages = false, bool $s } } - return remote_process($commands, $service->server, type_uuid: $service->uuid); + return remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged'); } } diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index a7fa4b8b2..3f4e96479 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -14,7 +14,7 @@ class StopService public string $jobQueue = 'high'; - public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true) + public function handle(Service $service, bool $deleteConnectedNetworks = false, bool $dockerCleanup = true) { try { $server = $service->destination->server; @@ -36,11 +36,11 @@ public function handle(Service $service, bool $isDeleteOperation = false, bool $ $this->stopContainersInParallel($containersToStop, $server); } - if ($isDeleteOperation) { + if ($deleteConnectedNetworks) { $service->deleteConnectedNetworks(); } if ($dockerCleanup) { - CleanupDocker::dispatch($server, true); + CleanupDocker::dispatch($server, false, false); } } catch (\Exception $e) { return $e->getMessage(); diff --git a/app/Console/Commands/CleanupRedis.php b/app/Console/Commands/CleanupRedis.php index 315d1adc7..a13cda0b8 100644 --- a/app/Console/Commands/CleanupRedis.php +++ b/app/Console/Commands/CleanupRedis.php @@ -7,26 +7,270 @@ class CleanupRedis extends Command { - protected $signature = 'cleanup:redis'; + protected $signature = 'cleanup:redis {--dry-run : Show what would be deleted without actually deleting} {--skip-overlapping : Skip overlapping queue cleanup}'; - protected $description = 'Cleanup Redis'; + protected $description = 'Cleanup Redis (Horizon jobs, metrics, overlapping queues, and related data)'; public function handle() { $redis = Redis::connection('horizon'); - $keys = $redis->keys('*'); $prefix = config('horizon.prefix'); + $dryRun = $this->option('dry-run'); + $skipOverlapping = $this->option('skip-overlapping'); + + if ($dryRun) { + $this->info('DRY RUN MODE - No data will be deleted'); + } + + $deletedCount = 0; + $totalKeys = 0; + + // Get all keys with the horizon prefix + $keys = $redis->keys('*'); + $totalKeys = count($keys); + + $this->info("Scanning {$totalKeys} keys for cleanup..."); + foreach ($keys as $key) { $keyWithoutPrefix = str_replace($prefix, '', $key); $type = $redis->command('type', [$keyWithoutPrefix]); + // Handle hash-type keys (individual jobs) if ($type === 5) { - $data = $redis->command('hgetall', [$keyWithoutPrefix]); - $status = data_get($data, 'status'); - if ($status === 'completed') { - $redis->command('del', [$keyWithoutPrefix]); + if ($this->shouldDeleteHashKey($redis, $keyWithoutPrefix, $dryRun)) { + $deletedCount++; + } + } + // Handle other key types (metrics, lists, etc.) + else { + if ($this->shouldDeleteOtherKey($redis, $keyWithoutPrefix, $key, $dryRun)) { + $deletedCount++; } } } + + // Clean up overlapping queues if not skipped + if (! $skipOverlapping) { + $this->info('Cleaning up overlapping queues...'); + $overlappingCleaned = $this->cleanupOverlappingQueues($redis, $prefix, $dryRun); + $deletedCount += $overlappingCleaned; + } + + if ($dryRun) { + $this->info("DRY RUN: Would delete {$deletedCount} out of {$totalKeys} keys"); + } else { + $this->info("Deleted {$deletedCount} out of {$totalKeys} keys"); + } + } + + private function shouldDeleteHashKey($redis, $keyWithoutPrefix, $dryRun) + { + $data = $redis->command('hgetall', [$keyWithoutPrefix]); + $status = data_get($data, 'status'); + + // Delete completed and failed jobs + if (in_array($status, ['completed', 'failed'])) { + if ($dryRun) { + $this->line("Would delete job: {$keyWithoutPrefix} (status: {$status})"); + } else { + $redis->command('del', [$keyWithoutPrefix]); + $this->line("Deleted job: {$keyWithoutPrefix} (status: {$status})"); + } + + return true; + } + + return false; + } + + private function shouldDeleteOtherKey($redis, $keyWithoutPrefix, $fullKey, $dryRun) + { + // Clean up various Horizon data structures + $patterns = [ + 'recent_jobs' => 'Recent jobs list', + 'failed_jobs' => 'Failed jobs list', + 'completed_jobs' => 'Completed jobs list', + 'job_classes' => 'Job classes metrics', + 'queues' => 'Queue metrics', + 'processes' => 'Process metrics', + 'supervisors' => 'Supervisor data', + 'metrics' => 'General metrics', + 'workload' => 'Workload data', + ]; + + foreach ($patterns as $pattern => $description) { + if (str_contains($keyWithoutPrefix, $pattern)) { + if ($dryRun) { + $this->line("Would delete {$description}: {$keyWithoutPrefix}"); + } else { + $redis->command('del', [$keyWithoutPrefix]); + $this->line("Deleted {$description}: {$keyWithoutPrefix}"); + } + + return true; + } + } + + // Clean up old timestamped data (older than 7 days) + if (preg_match('/(\d{10})/', $keyWithoutPrefix, $matches)) { + $timestamp = (int) $matches[1]; + $weekAgo = now()->subDays(7)->timestamp; + + if ($timestamp < $weekAgo) { + if ($dryRun) { + $this->line("Would delete old timestamped data: {$keyWithoutPrefix}"); + } else { + $redis->command('del', [$keyWithoutPrefix]); + $this->line("Deleted old timestamped data: {$keyWithoutPrefix}"); + } + + return true; + } + } + + return false; + } + + private function cleanupOverlappingQueues($redis, $prefix, $dryRun) + { + $cleanedCount = 0; + $queueKeys = []; + + // Find all queue-related keys + $allKeys = $redis->keys('*'); + foreach ($allKeys as $key) { + $keyWithoutPrefix = str_replace($prefix, '', $key); + if (str_contains($keyWithoutPrefix, 'queue:') || preg_match('/queues?[:\-]/', $keyWithoutPrefix)) { + $queueKeys[] = $keyWithoutPrefix; + } + } + + $this->info('Found '.count($queueKeys).' queue-related keys'); + + // Group queues by name pattern to find duplicates + $queueGroups = []; + foreach ($queueKeys as $queueKey) { + // Extract queue name (remove timestamps, suffixes) + $baseName = preg_replace('/[:\-]\d+$/', '', $queueKey); + $baseName = preg_replace('/[:\-](pending|reserved|delayed|processing)$/', '', $baseName); + + if (! isset($queueGroups[$baseName])) { + $queueGroups[$baseName] = []; + } + $queueGroups[$baseName][] = $queueKey; + } + + // Process each group for overlaps + foreach ($queueGroups as $baseName => $keys) { + if (count($keys) > 1) { + $cleanedCount += $this->deduplicateQueueGroup($redis, $baseName, $keys, $dryRun); + } + + // Also check for duplicate jobs within individual queues + foreach ($keys as $queueKey) { + $cleanedCount += $this->deduplicateQueueContents($redis, $queueKey, $dryRun); + } + } + + return $cleanedCount; + } + + private function deduplicateQueueGroup($redis, $baseName, $keys, $dryRun) + { + $cleanedCount = 0; + $this->line("Processing queue group: {$baseName} (".count($keys).' keys)'); + + // Sort keys to keep the most recent one + usort($keys, function ($a, $b) { + // Prefer keys without timestamps (they're usually the main queue) + $aHasTimestamp = preg_match('/\d{10}/', $a); + $bHasTimestamp = preg_match('/\d{10}/', $b); + + if ($aHasTimestamp && ! $bHasTimestamp) { + return 1; + } + if (! $aHasTimestamp && $bHasTimestamp) { + return -1; + } + + // If both have timestamps, prefer the newer one + if ($aHasTimestamp && $bHasTimestamp) { + preg_match('/(\d{10})/', $a, $aMatches); + preg_match('/(\d{10})/', $b, $bMatches); + + return ($bMatches[1] ?? 0) <=> ($aMatches[1] ?? 0); + } + + return strcmp($a, $b); + }); + + // Keep the first (preferred) key, remove others that are empty or redundant + $keepKey = array_shift($keys); + + foreach ($keys as $redundantKey) { + $type = $redis->command('type', [$redundantKey]); + $shouldDelete = false; + + if ($type === 1) { // LIST type + $length = $redis->command('llen', [$redundantKey]); + if ($length == 0) { + $shouldDelete = true; + } + } elseif ($type === 3) { // SET type + $count = $redis->command('scard', [$redundantKey]); + if ($count == 0) { + $shouldDelete = true; + } + } elseif ($type === 4) { // ZSET type + $count = $redis->command('zcard', [$redundantKey]); + if ($count == 0) { + $shouldDelete = true; + } + } + + if ($shouldDelete) { + if ($dryRun) { + $this->line(" Would delete empty queue: {$redundantKey}"); + } else { + $redis->command('del', [$redundantKey]); + $this->line(" Deleted empty queue: {$redundantKey}"); + } + $cleanedCount++; + } + } + + return $cleanedCount; + } + + private function deduplicateQueueContents($redis, $queueKey, $dryRun) + { + $cleanedCount = 0; + $type = $redis->command('type', [$queueKey]); + + if ($type === 1) { // LIST type - common for job queues + $length = $redis->command('llen', [$queueKey]); + if ($length > 1) { + $items = $redis->command('lrange', [$queueKey, 0, -1]); + $uniqueItems = array_unique($items); + + if (count($uniqueItems) < count($items)) { + $duplicates = count($items) - count($uniqueItems); + + if ($dryRun) { + $this->line(" Would remove {$duplicates} duplicate jobs from queue: {$queueKey}"); + } else { + // Rebuild the list with unique items + $redis->command('del', [$queueKey]); + foreach (array_reverse($uniqueItems) as $item) { + $redis->command('lpush', [$queueKey, $item]); + } + $this->line(" Removed {$duplicates} duplicate jobs from queue: {$queueKey}"); + } + $cleanedCount += $duplicates; + } + } + } + + return $cleanedCount; } } diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php index 0b5eef84d..81824675b 100644 --- a/app/Console/Commands/CleanupStuckedResources.php +++ b/app/Console/Commands/CleanupStuckedResources.php @@ -20,6 +20,7 @@ use App\Models\StandaloneMysql; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; +use App\Models\Team; use Illuminate\Console\Command; class CleanupStuckedResources extends Command @@ -36,6 +37,12 @@ public function handle() private function cleanup_stucked_resources() { try { + $teams = Team::all()->filter(function ($team) { + return $team->members()->count() === 0 && $team->servers()->count() === 0; + }); + foreach ($teams as $team) { + $team->delete(); + } $servers = Server::all()->filter(function ($server) { return $server->isFunctional(); }); diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index a954b10fd..1a7c0911f 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -36,24 +36,20 @@ public function handle() $this->servers = Server::all(); if (! isCloud()) { - $this->send_alive_signal(); + $this->sendAliveSignal(); get_public_ips(); } // Backward compatibility - $this->replace_slash_in_environment_name(); - $this->restore_coolify_db_backup(); - $this->update_user_emails(); + $this->replaceSlashInEnvironmentName(); + $this->restoreCoolifyDbBackup(); + $this->updateUserEmails(); // - $this->update_traefik_labels(); + $this->updateTraefikLabels(); if (! isCloud() || $this->option('force-cloud')) { - $this->cleanup_unused_network_from_coolify_proxy(); - } - if (isCloud()) { - $this->cleanup_unnecessary_dynamic_proxy_configuration(); - } else { - $this->cleanup_in_progress_application_deployments(); + $this->cleanupUnusedNetworkFromCoolifyProxy(); } + $this->call('cleanup:redis'); $this->call('cleanup:stucked-resources'); @@ -66,33 +62,35 @@ public function handle() if (isCloud()) { try { + $this->cleanupUnnecessaryDynamicProxyConfiguration(); $this->pullTemplatesFromCDN(); } catch (\Throwable $e) { echo "Could not pull templates from CDN: {$e->getMessage()}\n"; } + + return; } - if (! isCloud()) { - try { - $this->pullTemplatesFromCDN(); - } catch (\Throwable $e) { - echo "Could not pull templates from CDN: {$e->getMessage()}\n"; - } - try { - $localhost = $this->servers->where('id', 0)->first(); - $localhost->setupDynamicProxyConfiguration(); - } catch (\Throwable $e) { - echo "Could not setup dynamic configuration: {$e->getMessage()}\n"; - } - $settings = instanceSettings(); - if (! is_null(config('constants.coolify.autoupdate', null))) { - if (config('constants.coolify.autoupdate') == true) { - echo "Enabling auto-update\n"; - $settings->update(['is_auto_update_enabled' => true]); - } else { - echo "Disabling auto-update\n"; - $settings->update(['is_auto_update_enabled' => false]); - } + try { + $this->cleanupInProgressApplicationDeployments(); + $this->pullTemplatesFromCDN(); + } catch (\Throwable $e) { + echo "Could not pull templates from CDN: {$e->getMessage()}\n"; + } + try { + $localhost = $this->servers->where('id', 0)->first(); + $localhost->setupDynamicProxyConfiguration(); + } catch (\Throwable $e) { + echo "Could not setup dynamic configuration: {$e->getMessage()}\n"; + } + $settings = instanceSettings(); + if (! is_null(config('constants.coolify.autoupdate', null))) { + if (config('constants.coolify.autoupdate') == true) { + echo "Enabling auto-update\n"; + $settings->update(['is_auto_update_enabled' => true]); + } else { + echo "Disabling auto-update\n"; + $settings->update(['is_auto_update_enabled' => false]); } } } @@ -117,7 +115,7 @@ private function optimize() Artisan::call('optimize'); } - private function update_user_emails() + private function updateUserEmails() { try { User::whereRaw('email ~ \'[A-Z]\'')->get()->each(function (User $user) { @@ -128,7 +126,7 @@ private function update_user_emails() } } - private function update_traefik_labels() + private function updateTraefikLabels() { try { Server::where('proxy->type', 'TRAEFIK_V2')->update(['proxy->type' => 'TRAEFIK']); @@ -137,7 +135,7 @@ private function update_traefik_labels() } } - private function cleanup_unnecessary_dynamic_proxy_configuration() + private function cleanupUnnecessaryDynamicProxyConfiguration() { foreach ($this->servers as $server) { try { @@ -158,7 +156,7 @@ private function cleanup_unnecessary_dynamic_proxy_configuration() } } - private function cleanup_unused_network_from_coolify_proxy() + private function cleanupUnusedNetworkFromCoolifyProxy() { foreach ($this->servers as $server) { if (! $server->isFunctional()) { @@ -197,7 +195,7 @@ private function cleanup_unused_network_from_coolify_proxy() } } - private function restore_coolify_db_backup() + private function restoreCoolifyDbBackup() { if (version_compare('4.0.0-beta.179', config('constants.coolify.version'), '<=')) { try { @@ -223,7 +221,7 @@ private function restore_coolify_db_backup() } } - private function send_alive_signal() + private function sendAliveSignal() { $id = config('app.id'); $version = config('constants.coolify.version'); @@ -241,7 +239,7 @@ private function send_alive_signal() } } - private function cleanup_in_progress_application_deployments() + private function cleanupInProgressApplicationDeployments() { // Cleanup any failed deployments try { @@ -258,7 +256,7 @@ private function cleanup_in_progress_application_deployments() } } - private function replace_slash_in_environment_name() + private function replaceSlashInEnvironmentName() { if (version_compare('4.0.0-beta.298', config('constants.coolify.version'), '<=')) { $environments = Environment::all(); diff --git a/app/Console/Commands/RunScheduledJobsManually.php b/app/Console/Commands/RunScheduledJobsManually.php new file mode 100644 index 000000000..238bcbce3 --- /dev/null +++ b/app/Console/Commands/RunScheduledJobsManually.php @@ -0,0 +1,247 @@ +option('type'); + $frequency = $this->option('frequency'); + $chunkSize = (int) $this->option('chunk'); + $delay = (int) $this->option('delay'); + $maxJobs = $this->option('max') ? (int) $this->option('max') : null; + $dryRun = $this->option('dry-run'); + + $this->info('Starting manual execution of scheduled jobs...'.($dryRun ? ' (DRY RUN)' : '')); + $this->info("Type: {$type}".($frequency ? ", Frequency: {$frequency}" : '').", Chunk size: {$chunkSize}, Delay: {$delay}s".($maxJobs ? ", Max jobs: {$maxJobs}" : '').($dryRun ? ', Dry run: enabled' : '')); + + if ($dryRun) { + $this->warn('DRY RUN MODE: No jobs will actually be dispatched'); + } + + if ($type === 'all' || $type === 'backups') { + $this->runScheduledBackups($chunkSize, $delay, $maxJobs, $dryRun, $frequency); + } + + if ($type === 'all' || $type === 'tasks') { + $this->runScheduledTasks($chunkSize, $delay, $maxJobs, $dryRun, $frequency); + } + + $this->info('Completed manual execution of scheduled jobs.'.($dryRun ? ' (DRY RUN)' : '')); + } + + private function runScheduledBackups(int $chunkSize, int $delay, ?int $maxJobs = null, bool $dryRun = false, ?string $frequency = null): void + { + $this->info('Processing scheduled database backups...'); + + $query = ScheduledDatabaseBackup::where('enabled', true); + + if ($frequency) { + $query->where(function ($q) use ($frequency) { + // Handle human-readable frequency strings + if (in_array($frequency, ['daily', 'hourly', 'weekly', 'monthly', 'yearly', 'every_minute'])) { + $q->where('frequency', $frequency); + } else { + // Handle cron expressions + $q->where('frequency', $frequency); + } + }); + } + + $scheduled_backups = $query->get(); + + if ($scheduled_backups->isEmpty()) { + $this->info('No enabled scheduled backups found'.($frequency ? " with frequency '{$frequency}'" : '').'.'); + + return; + } + + $finalScheduledBackups = collect(); + + foreach ($scheduled_backups as $scheduled_backup) { + if (blank(data_get($scheduled_backup, 'database'))) { + $this->warn("Deleting backup {$scheduled_backup->id} - missing database"); + $scheduled_backup->delete(); + + continue; + } + + $server = $scheduled_backup->server(); + if (blank($server)) { + $this->warn("Deleting backup {$scheduled_backup->id} - missing server"); + $scheduled_backup->delete(); + + continue; + } + + if ($server->isFunctional() === false) { + $this->warn("Skipping backup {$scheduled_backup->id} - server not functional"); + + continue; + } + + if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) { + $this->warn("Skipping backup {$scheduled_backup->id} - subscription not paid"); + + continue; + } + + $finalScheduledBackups->push($scheduled_backup); + } + + if ($maxJobs && $finalScheduledBackups->count() > $maxJobs) { + $finalScheduledBackups = $finalScheduledBackups->take($maxJobs); + $this->info("Limited to {$maxJobs} scheduled backups for testing"); + } + + $this->info("Found {$finalScheduledBackups->count()} valid scheduled backups to process".($frequency ? " with frequency '{$frequency}'" : '')); + + $chunks = $finalScheduledBackups->chunk($chunkSize); + foreach ($chunks as $index => $chunk) { + $this->info('Processing backup batch '.($index + 1).' of '.$chunks->count()." ({$chunk->count()} items)"); + + foreach ($chunk as $scheduled_backup) { + try { + if ($dryRun) { + $this->info("🔍 Would dispatch backup job for: {$scheduled_backup->name} (ID: {$scheduled_backup->id}, Frequency: {$scheduled_backup->frequency})"); + } else { + DatabaseBackupJob::dispatch($scheduled_backup); + $this->info("✓ Dispatched backup job for: {$scheduled_backup->name} (ID: {$scheduled_backup->id}, Frequency: {$scheduled_backup->frequency})"); + } + } catch (\Exception $e) { + $this->error("✗ Failed to dispatch backup job for {$scheduled_backup->id}: ".$e->getMessage()); + Log::error('Error dispatching backup job: '.$e->getMessage()); + } + } + + if ($index < $chunks->count() - 1 && ! $dryRun) { + $this->info("Waiting {$delay} seconds before next batch..."); + sleep($delay); + } + } + } + + private function runScheduledTasks(int $chunkSize, int $delay, ?int $maxJobs = null, bool $dryRun = false, ?string $frequency = null): void + { + $this->info('Processing scheduled tasks...'); + + $query = ScheduledTask::where('enabled', true); + + if ($frequency) { + $query->where(function ($q) use ($frequency) { + // Handle human-readable frequency strings + if (in_array($frequency, ['daily', 'hourly', 'weekly', 'monthly', 'yearly', 'every_minute'])) { + $q->where('frequency', $frequency); + } else { + // Handle cron expressions + $q->where('frequency', $frequency); + } + }); + } + + $scheduled_tasks = $query->get(); + + if ($scheduled_tasks->isEmpty()) { + $this->info('No enabled scheduled tasks found'.($frequency ? " with frequency '{$frequency}'" : '').'.'); + + return; + } + + $finalScheduledTasks = collect(); + + foreach ($scheduled_tasks as $scheduled_task) { + $service = $scheduled_task->service; + $application = $scheduled_task->application; + + $server = $scheduled_task->server(); + if (blank($server)) { + $this->warn("Deleting task {$scheduled_task->id} - missing server"); + $scheduled_task->delete(); + + continue; + } + + if ($server->isFunctional() === false) { + $this->warn("Skipping task {$scheduled_task->id} - server not functional"); + + continue; + } + + if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) { + $this->warn("Skipping task {$scheduled_task->id} - subscription not paid"); + + continue; + } + + if (! $service && ! $application) { + $this->warn("Deleting task {$scheduled_task->id} - missing service and application"); + $scheduled_task->delete(); + + continue; + } + + if ($application && str($application->status)->contains('running') === false) { + $this->warn("Skipping task {$scheduled_task->id} - application not running"); + + continue; + } + + if ($service && str($service->status)->contains('running') === false) { + $this->warn("Skipping task {$scheduled_task->id} - service not running"); + + continue; + } + + $finalScheduledTasks->push($scheduled_task); + } + + if ($maxJobs && $finalScheduledTasks->count() > $maxJobs) { + $finalScheduledTasks = $finalScheduledTasks->take($maxJobs); + $this->info("Limited to {$maxJobs} scheduled tasks for testing"); + } + + $this->info("Found {$finalScheduledTasks->count()} valid scheduled tasks to process".($frequency ? " with frequency '{$frequency}'" : '')); + + $chunks = $finalScheduledTasks->chunk($chunkSize); + foreach ($chunks as $index => $chunk) { + $this->info('Processing task batch '.($index + 1).' of '.$chunks->count()." ({$chunk->count()} items)"); + + foreach ($chunk as $scheduled_task) { + try { + if ($dryRun) { + $this->info("🔍 Would dispatch task job for: {$scheduled_task->name} (ID: {$scheduled_task->id}, Frequency: {$scheduled_task->frequency})"); + } else { + ScheduledTaskJob::dispatch($scheduled_task); + $this->info("✓ Dispatched task job for: {$scheduled_task->name} (ID: {$scheduled_task->id}, Frequency: {$scheduled_task->frequency})"); + } + } catch (\Exception $e) { + $this->error("✗ Failed to dispatch task job for {$scheduled_task->id}: ".$e->getMessage()); + Log::error('Error dispatching task job: '.$e->getMessage()); + } + } + + if ($index < $chunks->count() - 1 && ! $dryRun) { + $this->info("Waiting {$delay} seconds before next batch..."); + sleep($delay); + } + } + } +} diff --git a/app/Console/Commands/ViewScheduledLogs.php b/app/Console/Commands/ViewScheduledLogs.php new file mode 100644 index 000000000..9ecf90716 --- /dev/null +++ b/app/Console/Commands/ViewScheduledLogs.php @@ -0,0 +1,278 @@ +option('date') ?: now()->format('Y-m-d'); + $logPaths = $this->getLogPaths($date); + + if (empty($logPaths)) { + $this->showAvailableLogFiles($date); + + return; + } + + $lines = $this->option('lines'); + $follow = $this->option('follow'); + + // Build grep filters + $filters = $this->buildFilters(); + $filterDescription = $this->getFilterDescription(); + $logTypeDescription = $this->getLogTypeDescription(); + + if ($follow) { + $this->info("Following {$logTypeDescription} logs for {$date}{$filterDescription} (Press Ctrl+C to stop)..."); + $this->line(''); + + if (count($logPaths) === 1) { + $logPath = $logPaths[0]; + if ($filters) { + passthru("tail -f {$logPath} | grep -E '{$filters}'"); + } else { + passthru("tail -f {$logPath}"); + } + } else { + // Multiple files - use multitail or tail with process substitution + $logPathsStr = implode(' ', $logPaths); + if ($filters) { + passthru("tail -f {$logPathsStr} | grep -E '{$filters}'"); + } else { + passthru("tail -f {$logPathsStr}"); + } + } + } else { + $this->info("Showing last {$lines} lines of {$logTypeDescription} logs for {$date}{$filterDescription}:"); + $this->line(''); + + if (count($logPaths) === 1) { + $logPath = $logPaths[0]; + if ($filters) { + passthru("tail -n {$lines} {$logPath} | grep -E '{$filters}'"); + } else { + passthru("tail -n {$lines} {$logPath}"); + } + } else { + // Multiple files - concatenate and sort by timestamp + $logPathsStr = implode(' ', $logPaths); + if ($filters) { + passthru("tail -n {$lines} {$logPathsStr} | sort | grep -E '{$filters}'"); + } else { + passthru("tail -n {$lines} {$logPathsStr} | sort"); + } + } + } + } + + private function getLogPaths(string $date): array + { + $paths = []; + + if ($this->option('errors')) { + // Error logs only + $errorPath = storage_path("logs/scheduled-errors-{$date}.log"); + if (File::exists($errorPath)) { + $paths[] = $errorPath; + } + } elseif ($this->option('all')) { + // Both normal and error logs + $normalPath = storage_path("logs/scheduled-{$date}.log"); + $errorPath = storage_path("logs/scheduled-errors-{$date}.log"); + + if (File::exists($normalPath)) { + $paths[] = $normalPath; + } + if (File::exists($errorPath)) { + $paths[] = $errorPath; + } + } else { + // Normal logs only (default) + $normalPath = storage_path("logs/scheduled-{$date}.log"); + if (File::exists($normalPath)) { + $paths[] = $normalPath; + } + } + + return $paths; + } + + private function showAvailableLogFiles(string $date): void + { + $logType = $this->getLogTypeDescription(); + $this->warn("No {$logType} logs found for date {$date}"); + + // Show available log files + $normalFiles = File::glob(storage_path('logs/scheduled-*.log')); + $errorFiles = File::glob(storage_path('logs/scheduled-errors-*.log')); + + if (! empty($normalFiles) || ! empty($errorFiles)) { + $this->info('Available scheduled log files:'); + + if (! empty($normalFiles)) { + $this->line(' Normal logs:'); + foreach ($normalFiles as $file) { + $basename = basename($file); + $this->line(" - {$basename}"); + } + } + + if (! empty($errorFiles)) { + $this->line(' Error logs:'); + foreach ($errorFiles as $file) { + $basename = basename($file); + $this->line(" - {$basename}"); + } + } + } + } + + private function getLogTypeDescription(): string + { + if ($this->option('errors')) { + return 'error'; + } elseif ($this->option('all')) { + return 'all'; + } else { + return 'normal'; + } + } + + private function buildFilters(): ?string + { + $filters = []; + + if ($taskName = $this->option('task-name')) { + $filters[] = '"task_name":"[^"]*'.preg_quote($taskName, '/').'[^"]*"'; + } + + if ($taskId = $this->option('task-id')) { + $filters[] = '"task_id":'.preg_quote($taskId, '/'); + } + + if ($backupName = $this->option('backup-name')) { + $filters[] = '"backup_name":"[^"]*'.preg_quote($backupName, '/').'[^"]*"'; + } + + if ($backupId = $this->option('backup-id')) { + $filters[] = '"backup_id":'.preg_quote($backupId, '/'); + } + + // Frequency filters + if ($this->option('hourly')) { + $filters[] = $this->getFrequencyPattern('hourly'); + } + + if ($this->option('daily')) { + $filters[] = $this->getFrequencyPattern('daily'); + } + + if ($this->option('weekly')) { + $filters[] = $this->getFrequencyPattern('weekly'); + } + + if ($this->option('monthly')) { + $filters[] = $this->getFrequencyPattern('monthly'); + } + + if ($frequency = $this->option('frequency')) { + $filters[] = '"frequency":"'.preg_quote($frequency, '/').'"'; + } + + return empty($filters) ? null : implode('|', $filters); + } + + private function getFrequencyPattern(string $type): string + { + $patterns = [ + 'hourly' => [ + '0 \* \* \* \*', // 0 * * * * + '@hourly', // @hourly + ], + 'daily' => [ + '0 0 \* \* \*', // 0 0 * * * + '@daily', // @daily + '@midnight', // @midnight + ], + 'weekly' => [ + '0 0 \* \* [0-6]', // 0 0 * * 0-6 (any day of week) + '@weekly', // @weekly + ], + 'monthly' => [ + '0 0 1 \* \*', // 0 0 1 * * (first of month) + '@monthly', // @monthly + ], + ]; + + $typePatterns = $patterns[$type] ?? []; + + // For grep, we need to match the frequency field in JSON + return '"frequency":"('.implode('|', $typePatterns).')"'; + } + + private function getFilterDescription(): string + { + $descriptions = []; + + if ($taskName = $this->option('task-name')) { + $descriptions[] = "task name: {$taskName}"; + } + + if ($taskId = $this->option('task-id')) { + $descriptions[] = "task ID: {$taskId}"; + } + + if ($backupName = $this->option('backup-name')) { + $descriptions[] = "backup name: {$backupName}"; + } + + if ($backupId = $this->option('backup-id')) { + $descriptions[] = "backup ID: {$backupId}"; + } + + // Frequency filters + if ($this->option('hourly')) { + $descriptions[] = 'hourly jobs'; + } + + if ($this->option('daily')) { + $descriptions[] = 'daily jobs'; + } + + if ($this->option('weekly')) { + $descriptions[] = 'weekly jobs'; + } + + if ($this->option('monthly')) { + $descriptions[] = 'monthly jobs'; + } + + if ($frequency = $this->option('frequency')) { + $descriptions[] = "frequency: {$frequency}"; + } + + return empty($descriptions) ? '' : ' (filtered by '.implode(', ', $descriptions).')'; + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 372a6c119..eda2fca74 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -6,23 +6,16 @@ use App\Jobs\CheckForUpdatesJob; use App\Jobs\CheckHelperImageJob; use App\Jobs\CleanupInstanceStuffsJob; -use App\Jobs\DatabaseBackupJob; -use App\Jobs\DockerCleanupJob; use App\Jobs\PullTemplatesFromCDN; use App\Jobs\RegenerateSslCertJob; -use App\Jobs\ScheduledTaskJob; -use App\Jobs\ServerCheckJob; -use App\Jobs\ServerPatchCheckJob; -use App\Jobs\ServerStorageCheckJob; +use App\Jobs\ScheduledJobManager; +use App\Jobs\ServerResourceManager; use App\Jobs\UpdateCoolifyJob; use App\Models\InstanceSettings; -use App\Models\ScheduledDatabaseBackup; -use App\Models\ScheduledTask; use App\Models\Server; use App\Models\Team; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; -use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Log; class Kernel extends ConsoleKernel @@ -52,7 +45,7 @@ protected function schedule(Schedule $schedule): void } // $this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly(); - $this->scheduleInstance->command('cleanup:redis')->hourly(); + $this->scheduleInstance->command('cleanup:redis')->weekly(); if (isDev()) { // Instance Jobs @@ -61,10 +54,10 @@ protected function schedule(Schedule $schedule): void $this->scheduleInstance->job(new CheckHelperImageJob)->everyTenMinutes()->onOneServer(); // Server Jobs - $this->checkResources(); + $this->scheduleInstance->job(new ServerResourceManager)->everyMinute()->onOneServer(); - $this->checkScheduledBackups(); - $this->checkScheduledTasks(); + // Scheduled Jobs (Backups & Tasks) + $this->scheduleInstance->job(new ScheduledJobManager)->everyMinute()->onOneServer(); $this->scheduleInstance->command('uploads:clear')->everyTwoMinutes(); @@ -79,12 +72,12 @@ protected function schedule(Schedule $schedule): void $this->scheduleUpdates(); // Server Jobs - $this->checkResources(); + $this->scheduleInstance->job(new ServerResourceManager)->everyMinute()->onOneServer(); $this->pullImages(); - $this->checkScheduledBackups(); - $this->checkScheduledTasks(); + // Scheduled Jobs (Backups & Tasks) + $this->scheduleInstance->job(new ScheduledJobManager)->everyMinute()->onOneServer(); $this->scheduleInstance->job(new RegenerateSslCertJob)->twiceDaily(); @@ -135,182 +128,6 @@ private function scheduleUpdates(): void } } - private function checkResources(): void - { - if (isCloud()) { - $servers = $this->allServers->whereRelation('team.subscription', 'stripe_invoice_paid', true)->get(); - $own = Team::find(0)->servers; - $servers = $servers->merge($own); - } else { - $servers = $this->allServers->get(); - } - - foreach ($servers as $server) { - try { - $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); - if (validate_timezone($serverTimezone) === false) { - $serverTimezone = config('app.timezone'); - } - - // Sentinel check - $lastSentinelUpdate = $server->sentinel_updated_at; - if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) { - // Check container status every minute if Sentinel does not activated - if (isCloud()) { - $this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyFiveMinutes()->onOneServer(); - } else { - $this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyMinute()->onOneServer(); - } - // $this->scheduleInstance->job(new \App\Jobs\ServerCheckNewJob($server))->everyFiveMinutes()->onOneServer(); - - $serverDiskUsageCheckFrequency = data_get($server->settings, 'server_disk_usage_check_frequency', '0 * * * *'); - if (isset(VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency])) { - $serverDiskUsageCheckFrequency = VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency]; - } - $this->scheduleInstance->job(new ServerStorageCheckJob($server))->cron($serverDiskUsageCheckFrequency)->timezone($serverTimezone)->onOneServer(); - } - - $dockerCleanupFrequency = data_get($server->settings, 'docker_cleanup_frequency', '0 * * * *'); - if (isset(VALID_CRON_STRINGS[$dockerCleanupFrequency])) { - $dockerCleanupFrequency = VALID_CRON_STRINGS[$dockerCleanupFrequency]; - } - $this->scheduleInstance->job(new DockerCleanupJob($server))->cron($dockerCleanupFrequency)->timezone($serverTimezone)->onOneServer(); - - // Server patch check - weekly - $this->scheduleInstance->job(new ServerPatchCheckJob($server))->weekly()->timezone($serverTimezone)->onOneServer(); - - // Cleanup multiplexed connections every hour - // $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer(); - - // Temporary solution until we have better memory management for Sentinel - if ($server->isSentinelEnabled()) { - $this->scheduleInstance->job(function () use ($server) { - $server->restartContainer('coolify-sentinel'); - })->daily()->onOneServer(); - } - } catch (\Exception $e) { - Log::error('Error checking resources: '.$e->getMessage()); - } - } - } - - private function checkScheduledBackups(): void - { - $scheduled_backups = ScheduledDatabaseBackup::where('enabled', true)->get(); - if ($scheduled_backups->isEmpty()) { - return; - } - $finalScheduledBackups = collect(); - foreach ($scheduled_backups as $scheduled_backup) { - if (blank(data_get($scheduled_backup, 'database'))) { - $scheduled_backup->delete(); - - continue; - } - $server = $scheduled_backup->server(); - if (blank($server)) { - $scheduled_backup->delete(); - - continue; - } - if ($server->isFunctional() === false) { - continue; - } - if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) { - continue; - } - $finalScheduledBackups->push($scheduled_backup); - } - - foreach ($finalScheduledBackups as $scheduled_backup) { - try { - if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) { - $scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency]; - } - $server = $scheduled_backup->server(); - $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); - - if (validate_timezone($serverTimezone) === false) { - $serverTimezone = config('app.timezone'); - } - - if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) { - $scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency]; - } - $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); - $this->scheduleInstance->job(new DatabaseBackupJob( - backup: $scheduled_backup - ))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer(); - } catch (\Exception $e) { - Log::error('Error scheduling backup: '.$e->getMessage()); - Log::error($e->getTraceAsString()); - } - } - } - - private function checkScheduledTasks(): void - { - $scheduled_tasks = ScheduledTask::where('enabled', true)->get(); - if ($scheduled_tasks->isEmpty()) { - return; - } - $finalScheduledTasks = collect(); - foreach ($scheduled_tasks as $scheduled_task) { - $service = $scheduled_task->service; - $application = $scheduled_task->application; - - $server = $scheduled_task->server(); - if (blank($server)) { - $scheduled_task->delete(); - - continue; - } - - if ($server->isFunctional() === false) { - continue; - } - - if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) { - continue; - } - - if (! $service && ! $application) { - $scheduled_task->delete(); - - continue; - } - - if ($application && str($application->status)->contains('running') === false) { - continue; - } - if ($service && str($service->status)->contains('running') === false) { - continue; - } - - $finalScheduledTasks->push($scheduled_task); - } - - foreach ($finalScheduledTasks as $scheduled_task) { - try { - $server = $scheduled_task->server(); - if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) { - $scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency]; - } - $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); - - if (validate_timezone($serverTimezone) === false) { - $serverTimezone = config('app.timezone'); - } - $this->scheduleInstance->job(new ScheduledTaskJob( - task: $scheduled_task - ))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer(); - } catch (\Exception $e) { - Log::error('Error scheduling task: '.$e->getMessage()); - Log::error($e->getTraceAsString()); - } - } - } - protected function commands(): void { $this->load(__DIR__.'/Commands'); diff --git a/app/Events/BackupCreated.php b/app/Events/BackupCreated.php index bc1ecee0d..9670f5c3c 100644 --- a/app/Events/BackupCreated.php +++ b/app/Events/BackupCreated.php @@ -7,8 +7,9 @@ use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; +use Laravel\Horizon\Contracts\Silenced; -class BackupCreated implements ShouldBroadcast +class BackupCreated implements ShouldBroadcast, Silenced { use Dispatchable, InteractsWithSockets, SerializesModels; diff --git a/app/Events/ServiceChecked.php b/app/Events/ServiceChecked.php index 3f130a0fb..86a27a892 100644 --- a/app/Events/ServiceChecked.php +++ b/app/Events/ServiceChecked.php @@ -7,8 +7,9 @@ use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; +use Laravel\Horizon\Contracts\Silenced; -class ServiceChecked implements ShouldBroadcast +class ServiceChecked implements ShouldBroadcast, Silenced { use Dispatchable, InteractsWithSockets, SerializesModels; diff --git a/app/Helpers/SshMultiplexingHelper.php b/app/Helpers/SshMultiplexingHelper.php index 8da476b9e..8caa2880a 100644 --- a/app/Helpers/SshMultiplexingHelper.php +++ b/app/Helpers/SshMultiplexingHelper.php @@ -103,7 +103,11 @@ public static function generateScpCommand(Server $server, string $source, string } $scp_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'), isScp: true); - $scp_command .= "{$source} {$server->user}@{$server->ip}:{$dest}"; + if ($server->isIpv6()) { + $scp_command .= "{$source} {$server->user}@[{$server->ip}]:{$dest}"; + } else { + $scp_command .= "{$source} {$server->user}@{$server->ip}:{$dest}"; + } return $scp_command; } diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 0860c7133..dcf0e5fbe 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -1699,10 +1699,10 @@ public function delete_by_uuid(Request $request) DeleteResourceJob::dispatch( resource: $application, - deleteConfigurations: $request->query->get('delete_configurations', true), deleteVolumes: $request->query->get('delete_volumes', true), - dockerCleanup: $request->query->get('docker_cleanup', true), - deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) + deleteConnectedNetworks: $request->query->get('delete_connected_networks', true), + deleteConfigurations: $request->query->get('delete_configurations', true), + dockerCleanup: $request->query->get('docker_cleanup', true) ); return response()->json([ diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 504665f6a..6ac052b3c 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -1608,10 +1608,10 @@ public function delete_by_uuid(Request $request) DeleteResourceJob::dispatch( resource: $database, - deleteConfigurations: $request->query->get('delete_configurations', true), deleteVolumes: $request->query->get('delete_volumes', true), - dockerCleanup: $request->query->get('docker_cleanup', true), - deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) + deleteConnectedNetworks: $request->query->get('delete_connected_networks', true), + deleteConfigurations: $request->query->get('delete_configurations', true), + dockerCleanup: $request->query->get('docker_cleanup', true) ); return response()->json([ diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 542be83de..34d8ee669 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -510,10 +510,10 @@ public function delete_by_uuid(Request $request) DeleteResourceJob::dispatch( resource: $service, - deleteConfigurations: $request->query->get('delete_configurations', true), deleteVolumes: $request->query->get('delete_volumes', true), - dockerCleanup: $request->query->get('docker_cleanup', true), - deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) + deleteConnectedNetworks: $request->query->get('delete_connected_networks', true), + deleteConfigurations: $request->query->get('delete_configurations', true), + dockerCleanup: $request->query->get('docker_cleanup', true) ); return response()->json([ diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 14a2950af..09007ad96 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -144,7 +144,7 @@ public function acceptInvitation() } } - public function revoke_invitation() + public function revokeInvitation() { $invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail(); $user = User::whereEmail($invitation->email)->firstOrFail(); diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php index 490b66e58..078494f82 100644 --- a/app/Http/Controllers/Webhook/Bitbucket.php +++ b/app/Http/Controllers/Webhook/Bitbucket.php @@ -143,12 +143,13 @@ public function manual(Request $request) ]); $pr_app->generate_preview_fqdn_compose(); } else { - ApplicationPreview::create([ + $pr_app = ApplicationPreview::create([ 'git_type' => 'bitbucket', 'application_id' => $application->id, 'pull_request_id' => $pull_request_id, 'pull_request_html_url' => $pull_request_html_url, ]); + $pr_app->generate_preview_fqdn(); } } $result = queue_application_deployment( diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php index 3c3d6e0b6..3e0c5a0b6 100644 --- a/app/Http/Controllers/Webhook/Gitea.php +++ b/app/Http/Controllers/Webhook/Gitea.php @@ -175,12 +175,13 @@ public function manual(Request $request) ]); $pr_app->generate_preview_fqdn_compose(); } else { - ApplicationPreview::create([ + $pr_app = ApplicationPreview::create([ 'git_type' => 'gitea', 'application_id' => $application->id, 'pull_request_id' => $pull_request_id, 'pull_request_html_url' => $pull_request_html_url, ]); + $pr_app->generate_preview_fqdn(); } } $result = queue_application_deployment( diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index 597ec023f..8872754e5 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -183,12 +183,13 @@ public function manual(Request $request) ]); $pr_app->generate_preview_fqdn_compose(); } else { - ApplicationPreview::create([ + $pr_app = ApplicationPreview::create([ 'git_type' => 'github', 'application_id' => $application->id, 'pull_request_id' => $pull_request_id, 'pull_request_html_url' => $pull_request_html_url, ]); + $pr_app->generate_preview_fqdn(); } } diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php index d6d12a05f..3187663d4 100644 --- a/app/Http/Controllers/Webhook/Gitlab.php +++ b/app/Http/Controllers/Webhook/Gitlab.php @@ -202,12 +202,13 @@ public function manual(Request $request) ]); $pr_app->generate_preview_fqdn_compose(); } else { - ApplicationPreview::create([ + $pr_app = ApplicationPreview::create([ 'git_type' => 'gitlab', 'application_id' => $application->id, 'pull_request_id' => $pull_request_id, 'pull_request_html_url' => $pull_request_html_url, ]); + $pr_app->generate_preview_fqdn(); } } $result = queue_application_deployment( diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php index 9e8652172..f07050d5e 100644 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -12,6 +12,6 @@ class VerifyCsrfToken extends Middleware * @var array */ protected $except = [ - // + 'webhooks/*', ]; } diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index eb7686c06..07d4ea9a0 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -30,6 +30,7 @@ use Illuminate\Support\Sleep; use Illuminate\Support\Str; use RuntimeException; +use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; use Throwable; use Visus\Cuid2\Cuid2; @@ -228,7 +229,14 @@ public function __construct(public int $application_deployment_queue_id) // Set preview fqdn if ($this->pull_request_id !== 0) { - $this->preview = $this->application->generate_preview_fqdn($this->pull_request_id); + $this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id); + if ($this->preview) { + if ($this->application->build_pack === 'dockercompose') { + $this->preview->generate_preview_fqdn_compose(); + } else { + $this->preview->generate_preview_fqdn(); + } + } if ($this->application->is_github_based()) { ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::IN_PROGRESS); } @@ -471,7 +479,7 @@ private function deploy_docker_compose_buildpack() } else { $composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id')); $this->save_environment_variables(); - if (! is_null($this->env_filename)) { + if (filled($this->env_filename)) { $services = collect(data_get($composeFile, 'services', [])); $services = $services->map(function ($service, $name) { $service['env_file'] = [$this->env_filename]; @@ -480,7 +488,7 @@ private function deploy_docker_compose_buildpack() }); $composeFile['services'] = $services->toArray(); } - if (is_null($composeFile)) { + if (empty($composeFile)) { $this->application_deployment_queue->addLogEntry('Failed to parse docker-compose file.'); $this->fail('Failed to parse docker-compose file.'); @@ -887,10 +895,6 @@ private function check_image_locally_or_remotely() private function save_environment_variables() { $envs = collect([]); - $local_branch = $this->branch; - if ($this->pull_request_id !== 0) { - $local_branch = "pull/{$this->pull_request_id}/head"; - } $sort = $this->application->settings->is_env_sorting_enabled; if ($sort) { $sorted_environment_variables = $this->application->environment_variables->sortBy('key'); @@ -899,6 +903,14 @@ private function save_environment_variables() $sorted_environment_variables = $this->application->environment_variables->sortBy('id'); $sorted_environment_variables_preview = $this->application->environment_variables_preview->sortBy('id'); } + if ($this->build_pack === 'dockercompose') { + $sorted_environment_variables = $sorted_environment_variables->filter(function ($env) { + return ! str($env->key)->startsWith('SERVICE_FQDN_') && ! str($env->key)->startsWith('SERVICE_URL_'); + }); + $sorted_environment_variables_preview = $sorted_environment_variables_preview->filter(function ($env) { + return ! str($env->key)->startsWith('SERVICE_FQDN_') && ! str($env->key)->startsWith('SERVICE_URL_'); + }); + } $ports = $this->application->main_port(); $coolify_envs = $this->generate_coolify_env_variables(); $coolify_envs->each(function ($item, $key) use ($envs) { @@ -908,17 +920,7 @@ private function save_environment_variables() $this->env_filename = '.env'; foreach ($sorted_environment_variables as $env) { - $real_value = $env->real_value; - if ($env->version === '4.0.0-beta.239') { - $real_value = $env->real_value; - } else { - if ($env->is_literal || $env->is_multiline) { - $real_value = '\''.$real_value.'\''; - } else { - $real_value = escapeEnvVariables($env->real_value); - } - } - $envs->push($env->key.'='.$real_value); + $envs->push($env->key.'='.$env->real_value); } // Add PORT if not exists, use the first port as default if ($this->build_pack !== 'dockercompose') { @@ -930,20 +932,28 @@ private function save_environment_variables() if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) { $envs->push('HOST=0.0.0.0'); } + + if ($this->build_pack === 'dockercompose') { + $domains = collect(json_decode($this->application->docker_compose_domains)) ?? collect([]); + + // Generate SERVICE_FQDN & SERVICE_URL for dockercompose + foreach ($domains as $forServiceName => $domain) { + $parsedDomain = data_get($domain, 'domain'); + if (filled($parsedDomain)) { + $parsedDomain = str($parsedDomain)->explode(',')->first(); + $coolifyUrl = Url::fromString($parsedDomain); + $coolifyScheme = $coolifyUrl->getScheme(); + $coolifyFqdn = $coolifyUrl->getHost(); + $coolifyUrl = $coolifyUrl->withScheme($coolifyScheme)->withHost($coolifyFqdn)->withPort(null); + $envs->push('SERVICE_URL_'.str($forServiceName)->upper().'='.$coolifyUrl->__toString()); + $envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.$coolifyFqdn); + } + } + } } else { $this->env_filename = ".env-pr-$this->pull_request_id"; foreach ($sorted_environment_variables_preview as $env) { - $real_value = $env->real_value; - if ($env->version === '4.0.0-beta.239') { - $real_value = $env->real_value; - } else { - if ($env->is_literal || $env->is_multiline) { - $real_value = '\''.$real_value.'\''; - } else { - $real_value = escapeEnvVariables($env->real_value); - } - } - $envs->push($env->key.'='.$real_value); + $envs->push($env->key.'='.$env->real_value); } // Add PORT if not exists, use the first port as default if ($this->build_pack !== 'dockercompose') { @@ -956,6 +966,23 @@ private function save_environment_variables() $envs->push('HOST=0.0.0.0'); } + if ($this->build_pack === 'dockercompose') { + $domains = collect(json_decode(data_get($this->preview, 'docker_compose_domains'))) ?? collect([]); + + // Generate SERVICE_FQDN & SERVICE_URL for dockercompose + foreach ($domains as $forServiceName => $domain) { + $parsedDomain = data_get($domain, 'domain'); + if (filled($parsedDomain)) { + $parsedDomain = str($parsedDomain)->explode(',')->first(); + $coolifyUrl = Url::fromString($parsedDomain); + $coolifyScheme = $coolifyUrl->getScheme(); + $coolifyFqdn = $coolifyUrl->getHost(); + $coolifyUrl = $coolifyUrl->withScheme($coolifyScheme)->withHost($coolifyFqdn)->withPort(null); + $envs->push('SERVICE_URL_'.str($forServiceName)->upper().'='.$coolifyUrl->__toString()); + $envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.$coolifyFqdn); + } + } + } } if ($envs->isEmpty()) { $this->env_filename = null; @@ -1367,9 +1394,16 @@ private function set_coolify_variables() $fqdn = $this->preview->fqdn; } if (isset($fqdn)) { - $this->coolify_variables .= "COOLIFY_FQDN={$fqdn} "; - $url = str($fqdn)->replace('http://', '')->replace('https://', ''); - $this->coolify_variables .= "COOLIFY_URL={$url} "; + $url = Url::fromString($fqdn); + $fqdn = $url->getHost(); + $url = $url->withHost($fqdn)->withPort(null)->__toString(); + if ((int) $this->application->compose_parsing_version >= 3) { + $this->coolify_variables .= "COOLIFY_URL={$url} "; + $this->coolify_variables .= "COOLIFY_FQDN={$fqdn} "; + } else { + $this->coolify_variables .= "COOLIFY_URL={$fqdn} "; + $this->coolify_variables .= "COOLIFY_FQDN={$url} "; + } } if (isset($this->application->git_branch)) { $this->coolify_variables .= "COOLIFY_BRANCH={$this->application->git_branch} "; @@ -1381,8 +1415,8 @@ private function check_git_if_build_needed() if (is_object($this->source) && $this->source->getMorphClass() === \App\Models\GithubApp::class && $this->source->is_public === false) { $repository = githubApi($this->source, "repos/{$this->customRepository}"); $data = data_get($repository, 'data'); - if (isset($data->id)) { - $repository_project_id = $data->id; + $repository_project_id = data_get($data, 'id'); + if (isset($repository_project_id)) { if (blank($this->application->repository_project_id) || $this->application->repository_project_id !== $repository_project_id) { $this->application->repository_project_id = $repository_project_id; $this->application->save(); @@ -1715,10 +1749,6 @@ private function generate_compose_file() { $this->create_workdir(); $ports = $this->application->main_port(); - $onlyPort = null; - if (count($ports) > 0) { - $onlyPort = $ports[0]; - } $persistent_storages = $this->generate_local_persistent_volumes(); $persistent_file_volumes = $this->application->fileStorages()->get(); $volume_names = $this->generate_local_persistent_volumes_only_volume_names(); @@ -2253,9 +2283,10 @@ private function build_image() $this->application_deployment_queue->addLogEntry('Building docker image completed.'); } - private function graceful_shutdown_container(string $containerName, int $timeout = 30) + private function graceful_shutdown_container(string $containerName) { try { + $timeout = isDev() ? 1 : 30; $this->execute_remote_command( ["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true], ["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true] diff --git a/app/Jobs/CleanupInstanceStuffsJob.php b/app/Jobs/CleanupInstanceStuffsJob.php index 53e344daf..60ae58489 100644 --- a/app/Jobs/CleanupInstanceStuffsJob.php +++ b/app/Jobs/CleanupInstanceStuffsJob.php @@ -23,7 +23,7 @@ public function __construct() {} public function middleware(): array { - return [(new WithoutOverlapping('cleanup-instance-stuffs'))->dontRelease()]; + return [(new WithoutOverlapping('cleanup-instance-stuffs'))->expireAfter(60)->dontRelease()]; } public function handle(): void diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index a6c423cac..428cdfda2 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -23,6 +23,8 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Str; +use Throwable; +use Visus\Cuid2\Cuid2; class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue { @@ -60,9 +62,16 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue public ?S3Storage $s3 = null; + public $timeout = 3600; + + public string $backup_log_uuid; + public function __construct(public ScheduledDatabaseBackup $backup) { $this->onQueue('high'); + $this->timeout = $backup->timeout; + + $this->backup_log_uuid = (string) new Cuid2; } public function handle(): void @@ -219,12 +228,8 @@ public function handle(): void $this->mongo_root_username = str($rootUsername)->after('MONGO_INITDB_ROOT_USERNAME=')->value(); } } - \Log::info('MongoDB credentials extracted from environment', [ - 'has_username' => filled($this->mongo_root_username), - 'has_password' => filled($this->mongo_root_password), - ]); + } catch (\Throwable $e) { - \Log::warning('Failed to extract MongoDB environment variables', ['error' => $e->getMessage()]); // Continue without env vars - will be handled in backup_standalone_mongodb method } } @@ -288,6 +293,7 @@ public function handle(): void } $this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ + 'uuid' => $this->backup_log_uuid, 'database_name' => $database, 'filename' => $this->backup_location, 'scheduled_database_backup_id' => $this->backup->id, @@ -307,6 +313,7 @@ public function handle(): void $this->backup_file = "/mongo-dump-$databaseName-".Carbon::now()->timestamp.'.tar.gz'; $this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ + 'uuid' => $this->backup_log_uuid, 'database_name' => $databaseName, 'filename' => $this->backup_location, 'scheduled_database_backup_id' => $this->backup->id, @@ -319,6 +326,7 @@ public function handle(): void } $this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ + 'uuid' => $this->backup_log_uuid, 'database_name' => $database, 'filename' => $this->backup_location, 'scheduled_database_backup_id' => $this->backup->id, @@ -331,6 +339,7 @@ public function handle(): void } $this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ + 'uuid' => $this->backup_log_uuid, 'database_name' => $database, 'filename' => $this->backup_location, 'scheduled_database_backup_id' => $this->backup->id, @@ -574,4 +583,18 @@ private function getFullImageName(): string return "{$helperImage}:{$latestVersion}"; } + + public function failed(?Throwable $exception): void + { + $log = ScheduledDatabaseBackupExecution::where('uuid', $this->backup_log_uuid)->first(); + + if ($log) { + $log->update([ + 'status' => 'failed', + 'message' => 'Job failed: '.($exception?->getMessage() ?? 'Unknown error'), + 'size' => 0, + 'filename' => null, + ]); + } + } } diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index 408bb2a7a..b9fbebcc9 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -8,6 +8,7 @@ use App\Actions\Service\DeleteService; use App\Actions\Service\StopService; use App\Models\Application; +use App\Models\ApplicationPreview; use App\Models\Service; use App\Models\StandaloneClickhouse; use App\Models\StandaloneDragonfly; @@ -30,11 +31,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct( - public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, - public bool $deleteConfigurations = true, + public Application|ApplicationPreview|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, public bool $deleteVolumes = true, - public bool $dockerCleanup = true, - public bool $deleteConnectedNetworks = true + public bool $deleteConnectedNetworks = true, + public bool $deleteConfigurations = true, + public bool $dockerCleanup = true ) { $this->onQueue('high'); } @@ -42,9 +43,16 @@ public function __construct( public function handle() { try { + // Handle ApplicationPreview instances separately + if ($this->resource instanceof ApplicationPreview) { + $this->deleteApplicationPreview(); + + return; + } + switch ($this->resource->type()) { case 'application': - StopApplication::run($this->resource, previewDeployments: true); + StopApplication::run($this->resource, previewDeployments: true, dockerCleanup: $this->dockerCleanup); break; case 'standalone-postgresql': case 'standalone-redis': @@ -54,11 +62,11 @@ public function handle() case 'standalone-keydb': case 'standalone-dragonfly': case 'standalone-clickhouse': - StopDatabase::run($this->resource, true); + StopDatabase::run($this->resource, dockerCleanup: $this->dockerCleanup); break; case 'service': - StopService::run($this->resource, true); - DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks); + StopService::run($this->resource, $this->deleteConnectedNetworks, $this->dockerCleanup); + DeleteService::run($this->resource, $this->deleteVolumes, $this->deleteConnectedNetworks, $this->deleteConfigurations, $this->dockerCleanup); return; } @@ -70,7 +78,7 @@ public function handle() $this->resource->deleteVolumes(); $this->resource->persistentStorages()->delete(); } - $this->resource->fileStorages()->delete(); + $this->resource->fileStorages()->delete(); // these are file mounts which should probably have their own flag $isDatabase = $this->resource instanceof StandalonePostgresql || $this->resource instanceof StandaloneRedis @@ -98,10 +106,61 @@ public function handle() if ($this->dockerCleanup) { $server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server'); if ($server) { - CleanupDocker::dispatch($server, true); + CleanupDocker::dispatch($server, false, false); } } Artisan::queue('cleanup:stucked-resources'); } } + + private function deleteApplicationPreview() + { + $application = $this->resource->application; + $server = $application->destination->server; + $pull_request_id = $this->resource->pull_request_id; + + // Ensure the preview is soft deleted (may already be done in Livewire component) + if (! $this->resource->trashed()) { + $this->resource->delete(); + } + + try { + if ($server->isSwarm()) { + instant_remote_process(["docker stack rm {$application->uuid}-{$pull_request_id}"], $server); + } else { + $containers = getCurrentApplicationContainerStatus($server, $application->id, $pull_request_id)->toArray(); + $this->stopPreviewContainers($containers, $server); + } + } catch (\Throwable $e) { + // Log the error but don't fail the job + ray('Error stopping preview containers: '.$e->getMessage()); + } + + // Finally, force delete to trigger resource cleanup + $this->resource->forceDelete(); + } + + private function stopPreviewContainers(array $containers, $server, int $timeout = 30) + { + if (empty($containers)) { + return; + } + + $containerNames = []; + foreach ($containers as $container) { + $containerNames[] = str_replace('/', '', $container['Names']); + } + + $containerList = implode(' ', array_map('escapeshellarg', $containerNames)); + $commands = [ + "docker stop --time=$timeout $containerList", + "docker rm -f $containerList", + ]; + + instant_remote_process( + command: $commands, + server: $server, + throwError: false + ); + } } diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index 00dfd150a..f3f3a2ae4 100644 --- a/app/Jobs/DockerCleanupJob.php +++ b/app/Jobs/DockerCleanupJob.php @@ -31,10 +31,15 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue public function middleware(): array { - return [(new WithoutOverlapping('docker-cleanup-'.$this->server->uuid))->dontRelease()]; + return [(new WithoutOverlapping('docker-cleanup-'.$this->server->uuid))->expireAfter(600)->dontRelease()]; } - public function __construct(public Server $server, public bool $manualCleanup = false) {} + public function __construct( + public Server $server, + public bool $manualCleanup = false, + public bool $deleteUnusedVolumes = false, + public bool $deleteUnusedNetworks = false + ) {} public function handle(): void { @@ -50,7 +55,11 @@ public function handle(): void $this->usageBefore = $this->server->getDiskUsage(); if ($this->manualCleanup || $this->server->settings->force_docker_cleanup) { - $cleanup_log = CleanupDocker::run(server: $this->server); + $cleanup_log = CleanupDocker::run( + server: $this->server, + deleteUnusedVolumes: $this->deleteUnusedVolumes, + deleteUnusedNetworks: $this->deleteUnusedNetworks + ); $usageAfter = $this->server->getDiskUsage(); $message = ($this->manualCleanup ? 'Manual' : 'Forced').' Docker cleanup job executed successfully. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.'; @@ -67,7 +76,11 @@ public function handle(): void } if (str($this->usageBefore)->isEmpty() || $this->usageBefore === null || $this->usageBefore === 0) { - $cleanup_log = CleanupDocker::run(server: $this->server); + $cleanup_log = CleanupDocker::run( + server: $this->server, + deleteUnusedVolumes: $this->deleteUnusedVolumes, + deleteUnusedNetworks: $this->deleteUnusedNetworks + ); $message = 'Docker cleanup job executed successfully, but no disk usage could be determined.'; $this->execution_log->update([ @@ -81,7 +94,11 @@ public function handle(): void } if ($this->usageBefore >= $this->server->settings->docker_cleanup_threshold) { - $cleanup_log = CleanupDocker::run(server: $this->server); + $cleanup_log = CleanupDocker::run( + server: $this->server, + deleteUnusedVolumes: $this->deleteUnusedVolumes, + deleteUnusedNetworks: $this->deleteUnusedNetworks + ); $usageAfter = $this->server->getDiskUsage(); $diskSaved = $this->usageBefore - $usageAfter; diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php index 65c72c8c9..3e3aa1eb7 100644 --- a/app/Jobs/PushServerUpdateJob.php +++ b/app/Jobs/PushServerUpdateJob.php @@ -21,8 +21,9 @@ use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Collection; +use Laravel\Horizon\Contracts\Silenced; -class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue +class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -70,7 +71,7 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue public function middleware(): array { - return [(new WithoutOverlapping('push-server-update-'.$this->server->uuid))->dontRelease()]; + return [(new WithoutOverlapping('push-server-update-'.$this->server->uuid))->expireAfter(30)->dontRelease()]; } public function backoff(): int diff --git a/app/Jobs/RestartProxyJob.php b/app/Jobs/RestartProxyJob.php index 49cfbba53..dba4f4ac8 100644 --- a/app/Jobs/RestartProxyJob.php +++ b/app/Jobs/RestartProxyJob.php @@ -23,7 +23,7 @@ class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue public function middleware(): array { - return [(new WithoutOverlapping('restart-proxy-'.$this->server->uuid))->dontRelease()]; + return [(new WithoutOverlapping('restart-proxy-'.$this->server->uuid))->expireAfter(60)->dontRelease()]; } public function __construct(public Server $server) {} diff --git a/app/Jobs/ScheduledJobManager.php b/app/Jobs/ScheduledJobManager.php new file mode 100644 index 000000000..b90e853fc --- /dev/null +++ b/app/Jobs/ScheduledJobManager.php @@ -0,0 +1,229 @@ +onQueue($this->determineQueue()); + } + + private function determineQueue(): string + { + $preferredQueue = 'crons'; + $fallbackQueue = 'high'; + + $configuredQueues = explode(',', env('HORIZON_QUEUES', 'high,default')); + + return in_array($preferredQueue, $configuredQueues) ? $preferredQueue : $fallbackQueue; + } + + /** + * Get the middleware the job should pass through. + */ + public function middleware(): array + { + return [ + (new WithoutOverlapping('scheduled-job-manager')) + ->releaseAfter(60), // Release the lock after 60 seconds if job fails + ]; + } + + public function handle(): void + { + // Freeze the execution time at the start of the job + $this->executionTime = Carbon::now(); + + // Process backups - don't let failures stop task processing + try { + $this->processScheduledBackups(); + } catch (\Exception $e) { + Log::channel('scheduled-errors')->error('Failed to process scheduled backups', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + } + + // Process tasks - don't let failures stop the job manager + try { + $this->processScheduledTasks(); + } catch (\Exception $e) { + Log::channel('scheduled-errors')->error('Failed to process scheduled tasks', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + } + } + + private function processScheduledBackups(): void + { + $backups = ScheduledDatabaseBackup::with(['database']) + ->where('enabled', true) + ->get(); + + foreach ($backups as $backup) { + try { + // Apply the same filtering logic as the original + if (! $this->shouldProcessBackup($backup)) { + continue; + } + + $server = $backup->server(); + $serverTimezone = data_get($server->settings, 'server_timezone', config('app.timezone')); + + if (validate_timezone($serverTimezone) === false) { + $serverTimezone = config('app.timezone'); + } + + $frequency = $backup->frequency; + if (isset(VALID_CRON_STRINGS[$frequency])) { + $frequency = VALID_CRON_STRINGS[$frequency]; + } + + if ($this->shouldRunNow($frequency, $serverTimezone)) { + DatabaseBackupJob::dispatch($backup); + } + } catch (\Exception $e) { + Log::channel('scheduled-errors')->error('Error processing backup', [ + 'backup_id' => $backup->id, + 'error' => $e->getMessage(), + ]); + } + } + } + + private function processScheduledTasks(): void + { + $tasks = ScheduledTask::with(['service', 'application']) + ->where('enabled', true) + ->get(); + + foreach ($tasks as $task) { + try { + if (! $this->shouldProcessTask($task)) { + continue; + } + + $server = $task->server(); + $serverTimezone = data_get($server->settings, 'server_timezone', config('app.timezone')); + + if (validate_timezone($serverTimezone) === false) { + $serverTimezone = config('app.timezone'); + } + + $frequency = $task->frequency; + if (isset(VALID_CRON_STRINGS[$frequency])) { + $frequency = VALID_CRON_STRINGS[$frequency]; + } + + if ($this->shouldRunNow($frequency, $serverTimezone)) { + ScheduledTaskJob::dispatch($task); + } + } catch (\Exception $e) { + Log::channel('scheduled-errors')->error('Error processing task', [ + 'task_id' => $task->id, + 'error' => $e->getMessage(), + ]); + } + } + } + + private function shouldProcessBackup(ScheduledDatabaseBackup $backup): bool + { + if (blank(data_get($backup, 'database'))) { + $backup->delete(); + + return false; + } + + $server = $backup->server(); + if (blank($server)) { + $backup->delete(); + + return false; + } + + if ($server->isFunctional() === false) { + return false; + } + + if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) { + return false; + } + + return true; + } + + private function shouldProcessTask(ScheduledTask $task): bool + { + $service = $task->service; + $application = $task->application; + + $server = $task->server(); + if (blank($server)) { + $task->delete(); + + return false; + } + + if ($server->isFunctional() === false) { + return false; + } + + if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) { + return false; + } + + if (! $service && ! $application) { + $task->delete(); + + return false; + } + + if ($application && str($application->status)->contains('running') === false) { + return false; + } + + if ($service && str($service->status)->contains('running') === false) { + return false; + } + + return true; + } + + private function shouldRunNow(string $frequency, string $timezone): bool + { + $cron = new CronExpression($frequency); + + // Use the frozen execution time, not the current time + // Fallback to current time if execution time is not set (shouldn't happen) + $baseTime = $this->executionTime ?? Carbon::now(); + $executionTime = $baseTime->copy()->setTimezone($timezone); + + return $cron->isDue($executionTime); + } +} diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php index dcbac9ac3..499035237 100644 --- a/app/Jobs/ServerCheckJob.php +++ b/app/Jobs/ServerCheckJob.php @@ -28,7 +28,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue public function middleware(): array { - return [(new WithoutOverlapping('server-check-'.$this->server->uuid))->dontRelease()]; + return [(new WithoutOverlapping('server-check-'.$this->server->uuid))->expireAfter(60)->dontRelease()]; } public function __construct(public Server $server) {} diff --git a/app/Jobs/ServerPatchCheckJob.php b/app/Jobs/ServerPatchCheckJob.php index 7d1b853b1..18999c009 100644 --- a/app/Jobs/ServerPatchCheckJob.php +++ b/app/Jobs/ServerPatchCheckJob.php @@ -19,11 +19,11 @@ class ServerPatchCheckJob implements ShouldBeEncrypted, ShouldQueue public $tries = 3; - public $timeout = 600; // 10 minutes timeout + public $timeout = 600; public function middleware(): array { - return [(new WithoutOverlapping('server-patch-check-'.$this->server->uuid))->dontRelease()]; + return [(new WithoutOverlapping('server-patch-check-'.$this->server->uuid))->expireAfter(600)->dontRelease()]; } public function __construct(public Server $server) {} diff --git a/app/Jobs/ServerResourceManager.php b/app/Jobs/ServerResourceManager.php new file mode 100644 index 000000000..8a4b55a3f --- /dev/null +++ b/app/Jobs/ServerResourceManager.php @@ -0,0 +1,162 @@ +onQueue('high'); + } + + /** + * Get the middleware the job should pass through. + */ + public function middleware(): array + { + return [ + (new WithoutOverlapping('server-resource-manager')) + ->releaseAfter(60), + ]; + } + + public function handle(): void + { + // Freeze the execution time at the start of the job + $this->executionTime = Carbon::now(); + + $this->settings = instanceSettings(); + $this->instanceTimezone = $this->settings->instance_timezone ?: config('app.timezone'); + + if (validate_timezone($this->instanceTimezone) === false) { + $this->instanceTimezone = config('app.timezone'); + } + + // Process server checks - don't let failures stop the job + try { + $this->processServerChecks(); + } catch (\Exception $e) { + Log::channel('scheduled-errors')->error('Failed to process server checks', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + } + } + + private function processServerChecks(): void + { + $servers = $this->getServers(); + + foreach ($servers as $server) { + try { + $this->processServer($server); + } catch (\Exception $e) { + Log::channel('scheduled-errors')->error('Error processing server', [ + 'server_id' => $server->id, + 'server_name' => $server->name, + 'error' => $e->getMessage(), + ]); + } + } + } + + private function getServers() + { + $allServers = Server::where('ip', '!=', '1.2.3.4'); + + if (isCloud()) { + $servers = $allServers->whereRelation('team.subscription', 'stripe_invoice_paid', true)->get(); + $own = Team::find(0)->servers; + + return $servers->merge($own); + } else { + return $allServers->get(); + } + } + + private function processServer(Server $server): void + { + $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); + if (validate_timezone($serverTimezone) === false) { + $serverTimezone = config('app.timezone'); + } + + // Sentinel check + $lastSentinelUpdate = $server->sentinel_updated_at; + if (Carbon::parse($lastSentinelUpdate)->isBefore($this->executionTime->subSeconds($server->waitBeforeDoingSshCheck()))) { + // Dispatch ServerCheckJob if due + $checkFrequency = isCloud() ? '*/5 * * * *' : '* * * * *'; // Every 5 min for cloud, every minute for self-hosted + if ($this->shouldRunNow($checkFrequency, $serverTimezone)) { + ServerCheckJob::dispatch($server); + } + + // Dispatch ServerStorageCheckJob if due + $serverDiskUsageCheckFrequency = data_get($server->settings, 'server_disk_usage_check_frequency', '0 * * * *'); + if (isset(VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency])) { + $serverDiskUsageCheckFrequency = VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency]; + } + if ($this->shouldRunNow($serverDiskUsageCheckFrequency, $serverTimezone)) { + ServerStorageCheckJob::dispatch($server); + } + } + + // Dispatch DockerCleanupJob if due + $dockerCleanupFrequency = data_get($server->settings, 'docker_cleanup_frequency', '0 * * * *'); + if (isset(VALID_CRON_STRINGS[$dockerCleanupFrequency])) { + $dockerCleanupFrequency = VALID_CRON_STRINGS[$dockerCleanupFrequency]; + } + if ($this->shouldRunNow($dockerCleanupFrequency, $serverTimezone)) { + DockerCleanupJob::dispatch($server, false, $server->settings->delete_unused_volumes, $server->settings->delete_unused_networks); + } + + // Dispatch ServerPatchCheckJob if due (weekly) + if ($this->shouldRunNow('0 0 * * 0', $serverTimezone)) { // Weekly on Sunday at midnight + ServerPatchCheckJob::dispatch($server); + } + + // Dispatch Sentinel restart if due (daily for Sentinel-enabled servers) + if ($server->isSentinelEnabled() && $this->shouldRunNow('0 0 * * *', $serverTimezone)) { + dispatch(function () use ($server) { + $server->restartContainer('coolify-sentinel'); + }); + } + } + + private function shouldRunNow(string $frequency, string $timezone): bool + { + $cron = new CronExpression($frequency); + + // Use the frozen execution time, not the current time + $baseTime = $this->executionTime ?? Carbon::now(); + $executionTime = $baseTime->copy()->setTimezone($timezone); + + return $cron->isDue($executionTime); + } +} diff --git a/app/Jobs/ServerStorageCheckJob.php b/app/Jobs/ServerStorageCheckJob.php index 9a8d86be1..9d45491c6 100644 --- a/app/Jobs/ServerStorageCheckJob.php +++ b/app/Jobs/ServerStorageCheckJob.php @@ -11,8 +11,9 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\RateLimiter; +use Laravel\Horizon\Contracts\Silenced; -class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue +class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue, Silenced { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index 897e6cf06..128321ed2 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -254,10 +254,9 @@ public function submitSmtp() 'smtpEncryption.required' => 'Encryption type is required.', ]); - $this->settings->resend_enabled = false; - $this->settings->use_instance_email_settings = false; - $this->resendEnabled = false; - $this->useInstanceEmailSettings = false; + if ($this->smtpEnabled) { + $this->settings->resend_enabled = $this->resendEnabled = false; + } $this->settings->smtp_enabled = $this->smtpEnabled; $this->settings->smtp_from_address = $this->smtpFromAddress; @@ -293,11 +292,9 @@ public function submitResend() 'smtpFromAddress.email' => 'Please enter a valid email address.', 'smtpFromName.required' => 'From Name is required.', ]); - - $this->settings->smtp_enabled = false; - $this->settings->use_instance_email_settings = false; - $this->smtpEnabled = false; - $this->useInstanceEmailSettings = false; + if ($this->resendEnabled) { + $this->settings->smtp_enabled = $this->smtpEnabled = false; + } $this->settings->resend_enabled = $this->resendEnabled; $this->settings->resend_api_key = $this->resendApiKey; diff --git a/app/Livewire/Project/Application/Deployment/Index.php b/app/Livewire/Project/Application/Deployment/Index.php index c957615ac..5b621cb95 100644 --- a/app/Livewire/Project/Application/Deployment/Index.php +++ b/app/Livewire/Project/Application/Deployment/Index.php @@ -18,11 +18,13 @@ class Index extends Component public int $skip = 0; - public int $default_take = 10; + public int $defaultTake = 10; - public bool $show_next = false; + public bool $showNext = false; - public bool $show_prev = false; + public bool $showPrev = false; + + public int $currentPage = 1; public ?string $pull_request_id = null; @@ -51,68 +53,111 @@ public function mount() if (! $application) { return redirect()->route('dashboard'); } - ['deployments' => $deployments, 'count' => $count] = $application->deployments(0, $this->default_take); + // Validate pull request ID from URL parameters + if ($this->pull_request_id !== null && $this->pull_request_id !== '') { + if (! is_numeric($this->pull_request_id) || (float) $this->pull_request_id <= 0 || (float) $this->pull_request_id != (int) $this->pull_request_id) { + $this->pull_request_id = null; + $this->dispatch('error', 'Invalid Pull Request ID in URL. Filter cleared.'); + } else { + // Ensure it's stored as a string representation of a positive integer + $this->pull_request_id = (string) (int) $this->pull_request_id; + } + } + + ['deployments' => $deployments, 'count' => $count] = $application->deployments(0, $this->defaultTake, $this->pull_request_id); $this->application = $application; $this->deployments = $deployments; $this->deployments_count = $count; $this->current_url = url()->current(); - $this->show_pull_request_only(); - $this->show_more(); + $this->updateCurrentPage(); + $this->showMore(); } - private function show_pull_request_only() - { - if ($this->pull_request_id) { - $this->deployments = $this->deployments->where('pull_request_id', $this->pull_request_id); - } - } - - private function show_more() + private function showMore() { if ($this->deployments->count() !== 0) { - $this->show_next = true; - if ($this->deployments->count() < $this->default_take) { - $this->show_next = false; + $this->showNext = true; + if ($this->deployments->count() < $this->defaultTake) { + $this->showNext = false; } return; } } - public function reload_deployments() + public function reloadDeployments() { - $this->load_deployments(); + $this->loadDeployments(); } - public function previous_page(?int $take = null) + public function previousPage(?int $take = null) { if ($take) { $this->skip = $this->skip - $take; } - $this->skip = $this->skip - $this->default_take; + $this->skip = $this->skip - $this->defaultTake; if ($this->skip < 0) { - $this->show_prev = false; + $this->showPrev = false; $this->skip = 0; } - $this->load_deployments(); + $this->updateCurrentPage(); + $this->loadDeployments(); } - public function next_page(?int $take = null) + public function nextPage(?int $take = null) { if ($take) { $this->skip = $this->skip + $take; } - $this->show_prev = true; - $this->load_deployments(); + $this->showPrev = true; + $this->updateCurrentPage(); + $this->loadDeployments(); } - public function load_deployments() + public function loadDeployments() { - ['deployments' => $deployments, 'count' => $count] = $this->application->deployments($this->skip, $this->default_take); + ['deployments' => $deployments, 'count' => $count] = $this->application->deployments($this->skip, $this->defaultTake, $this->pull_request_id); $this->deployments = $deployments; $this->deployments_count = $count; - $this->show_pull_request_only(); - $this->show_more(); + $this->showMore(); + } + + public function updatedPullRequestId($value) + { + // Sanitize and validate the pull request ID + if ($value !== null && $value !== '') { + // Check if it's numeric and positive + if (! is_numeric($value) || (float) $value <= 0 || (float) $value != (int) $value) { + $this->pull_request_id = null; + $this->dispatch('error', 'Invalid Pull Request ID. Please enter a valid positive number.'); + + return; + } + // Ensure it's stored as a string representation of a positive integer + $this->pull_request_id = (string) (int) $value; + } else { + $this->pull_request_id = null; + } + + // Reset pagination when filter changes + $this->skip = 0; + $this->showPrev = false; + $this->updateCurrentPage(); + $this->loadDeployments(); + } + + public function clearFilter() + { + $this->pull_request_id = null; + $this->skip = 0; + $this->showPrev = false; + $this->updateCurrentPage(); + $this->loadDeployments(); + } + + private function updateCurrentPage() + { + $this->currentPage = intval($this->skip / $this->defaultTake) + 1; } public function render() diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 74f47232c..58a35caa0 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -4,6 +4,7 @@ use App\Actions\Application\GenerateConfig; use App\Models\Application; +use App\Models\EnvironmentVariable; use Illuminate\Support\Collection; use Livewire\Component; use Spatie\Url\Url; @@ -156,6 +157,14 @@ public function mount() $this->application->settings->save(); } $this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : []; + // Convert service names with dots to use underscores for HTML form binding + $sanitizedDomains = []; + foreach ($this->parsedServiceDomains as $serviceName => $domain) { + $sanitizedKey = str($serviceName)->slug('_')->toString(); + $sanitizedDomains[$sanitizedKey] = $domain; + } + $this->parsedServiceDomains = $sanitizedDomains; + $this->ports_exposes = $this->application->ports_exposes; $this->is_preserve_repository_enabled = $this->application->settings->is_preserve_repository_enabled; $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled; @@ -206,25 +215,21 @@ public function instantSave() } - public function loadComposeFile($isInit = false) + public function loadComposeFile($isInit = false, $showToast = true) { try { if ($isInit && $this->application->docker_compose_raw) { return; } - // Must reload the application to get the latest database changes - // Why? Not sure, but it works. - // $this->application->refresh(); - ['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation] = $this->application->loadComposeFile($isInit); if (is_null($this->parsedServices)) { - $this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.'); + $showToast && $this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.'); return; } $this->application->parse(); - $this->dispatch('success', 'Docker compose file loaded.'); + $showToast && $this->dispatch('success', 'Docker compose file loaded.'); $this->dispatch('compose_loaded'); $this->dispatch('refreshStorages'); $this->dispatch('refreshEnvs'); @@ -242,12 +247,31 @@ public function generateDomain(string $serviceName) { $uuid = new Cuid2; $domain = generateFqdn($this->application->destination->server, $uuid); - $this->parsedServiceDomains[$serviceName]['domain'] = $domain; - $this->application->docker_compose_domains = json_encode($this->parsedServiceDomains); + $sanitizedKey = str($serviceName)->slug('_')->toString(); + $this->parsedServiceDomains[$sanitizedKey]['domain'] = $domain; + + // Convert back to original service names for storage + $originalDomains = []; + foreach ($this->parsedServiceDomains as $key => $value) { + // Find the original service name by checking parsed services + $originalServiceName = $key; + if (isset($this->parsedServices['services'])) { + foreach ($this->parsedServices['services'] as $originalName => $service) { + if (str($originalName)->slug('_')->toString() === $key) { + $originalServiceName = $originalName; + break; + } + } + } + $originalDomains[$originalServiceName] = $value; + } + + $this->application->docker_compose_domains = json_encode($originalDomains); $this->application->save(); $this->dispatch('success', 'Domain generated.'); if ($this->application->build_pack === 'dockercompose') { - $this->loadComposeFile(); + $this->updateServiceEnvironmentVariables(); + $this->loadComposeFile(showToast: false); } return $domain; @@ -429,9 +453,25 @@ public function submit($showToaster = true) $this->application->publish_directory = rtrim($this->application->publish_directory, '/'); } if ($this->application->build_pack === 'dockercompose') { - $this->application->docker_compose_domains = json_encode($this->parsedServiceDomains); + // Convert sanitized service names back to original names for storage + $originalDomains = []; + foreach ($this->parsedServiceDomains as $key => $value) { + // Find the original service name by checking parsed services + $originalServiceName = $key; + if (isset($this->parsedServices['services'])) { + foreach ($this->parsedServices['services'] as $originalName => $service) { + if (str($originalName)->slug('_')->toString() === $key) { + $originalServiceName = $originalName; + break; + } + } + } + $originalDomains[$originalServiceName] = $value; + } - foreach ($this->parsedServiceDomains as $serviceName => $service) { + $this->application->docker_compose_domains = json_encode($originalDomains); + + foreach ($originalDomains as $serviceName => $service) { $domain = data_get($service, 'domain'); if ($domain) { if (! validate_dns_entry($domain, $this->application->destination->server)) { @@ -446,6 +486,12 @@ public function submit($showToaster = true) } $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); + + // Update SERVICE_FQDN_ and SERVICE_URL_ environment variables for Docker Compose applications + if ($this->application->build_pack === 'dockercompose') { + $this->updateServiceEnvironmentVariables(); + } + $showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!'); } catch (\Throwable $e) { $originalFqdn = $this->application->getOriginal('fqdn'); @@ -471,4 +517,85 @@ public function downloadConfig() 'Content-Disposition' => 'attachment; filename='.$fileName, ]); } + + private function updateServiceEnvironmentVariables() + { + $domains = collect(json_decode($this->application->docker_compose_domains, true)) ?? collect([]); + + foreach ($domains as $serviceName => $service) { + $serviceNameFormatted = str($serviceName)->upper()->replace('-', '_'); + $domain = data_get($service, 'domain'); + + if ($domain) { + // Create or update SERVICE_FQDN_ and SERVICE_URL_ variables + $fqdn = Url::fromString($domain); + $port = $fqdn->getPort(); + $path = $fqdn->getPath(); + $fqdnValue = $fqdn->getScheme().'://'.$fqdn->getHost(); + if ($path !== '/') { + $fqdnValue = $fqdnValue.$path; + } + $urlValue = str($domain)->after('://'); + if ($path !== '/') { + $urlValue = $urlValue.$path; + } + + // Create/update SERVICE_FQDN_ + EnvironmentVariable::updateOrCreate([ + 'resourceable_type' => Application::class, + 'resourceable_id' => $this->application->id, + 'key' => "SERVICE_FQDN_{$serviceNameFormatted}", + ], [ + 'value' => $fqdnValue, + 'is_build_time' => false, + 'is_preview' => false, + ]); + + // Create/update SERVICE_URL_ + EnvironmentVariable::updateOrCreate([ + 'resourceable_type' => Application::class, + 'resourceable_id' => $this->application->id, + 'key' => "SERVICE_URL_{$serviceNameFormatted}", + ], [ + 'value' => $urlValue, + 'is_build_time' => false, + 'is_preview' => false, + ]); + + // Create/update port-specific variables if port exists + if ($port) { + EnvironmentVariable::updateOrCreate([ + 'resourceable_type' => Application::class, + 'resourceable_id' => $this->application->id, + 'key' => "SERVICE_FQDN_{$serviceNameFormatted}_{$port}", + ], [ + 'value' => $fqdnValue, + 'is_build_time' => false, + 'is_preview' => false, + ]); + + EnvironmentVariable::updateOrCreate([ + 'resourceable_type' => Application::class, + 'resourceable_id' => $this->application->id, + 'key' => "SERVICE_URL_{$serviceNameFormatted}_{$port}", + ], [ + 'value' => $urlValue, + 'is_build_time' => false, + 'is_preview' => false, + ]); + } + } else { + // Delete SERVICE_FQDN_ and SERVICE_URL_ variables if domain is removed + EnvironmentVariable::where('resourceable_type', Application::class) + ->where('resourceable_id', $this->application->id) + ->where('key', 'LIKE', "SERVICE_FQDN_{$serviceNameFormatted}%") + ->delete(); + + EnvironmentVariable::where('resourceable_type', Application::class) + ->where('resourceable_id', $this->application->id) + ->where('key', 'LIKE', "SERVICE_URL_{$serviceNameFormatted}%") + ->delete(); + } + } + } } diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index b2c1cf8e1..62b1f1929 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -3,11 +3,11 @@ namespace App\Livewire\Project\Application; use App\Actions\Docker\GetContainersStatus; +use App\Jobs\DeleteResourceJob; use App\Models\Application; use App\Models\ApplicationPreview; use Illuminate\Support\Collection; use Livewire\Component; -use Spatie\Url\Url; use Visus\Cuid2\Cuid2; class Previews extends Component @@ -87,18 +87,9 @@ public function generate_preview($preview_id) return; } - $fqdn = generateFqdn($this->application->destination->server, $this->application->uuid); - $url = Url::fromString($fqdn); - $template = $this->application->preview_url_template; - $host = $url->getHost(); - $schema = $url->getScheme(); - $random = new Cuid2; - $preview_fqdn = str_replace('{{random}}', $random, $template); - $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); - $preview_fqdn = str_replace('{{pr_id}}', $preview->pull_request_id, $preview_fqdn); - $preview_fqdn = "$schema://$preview_fqdn"; - $preview->fqdn = $preview_fqdn; - $preview->save(); + $preview->generate_preview_fqdn(); + $this->application->refresh(); + $this->dispatch('update_links'); $this->dispatch('success', 'Domain generated.'); } @@ -128,7 +119,7 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null) 'pull_request_html_url' => $pull_request_html_url, ]); } - $this->application->generate_preview_fqdn($pull_request_id); + $found->generate_preview_fqdn(); $this->application->refresh(); $this->dispatch('update_links'); $this->dispatch('success', 'Preview added.'); @@ -215,48 +206,28 @@ public function stop(int $pull_request_id) public function delete(int $pull_request_id) { try { - $server = $this->application->destination->server; + $preview = ApplicationPreview::where('application_id', $this->application->id) + ->where('pull_request_id', $pull_request_id) + ->first(); - if ($this->application->destination->server->isSwarm()) { - instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server); - } else { - $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray(); - $this->stopContainers($containers, $server); + if (! $preview) { + $this->dispatch('error', 'Preview not found.'); + + return; } - ApplicationPreview::where('application_id', $this->application->id) - ->where('pull_request_id', $pull_request_id) - ->first() - ->delete(); + // Soft delete immediately for instant UI feedback + $preview->delete(); - $this->application->refresh(); + // Dispatch the job for async cleanup (container stopping + force delete) + DeleteResourceJob::dispatch($preview); + + // Refresh the application and its previews relationship to reflect the soft delete + $this->application->load('previews'); $this->dispatch('update_links'); - $this->dispatch('success', 'Preview deleted.'); + $this->dispatch('success', 'Preview deletion started. It may take a few moments to complete.'); } catch (\Throwable $e) { return handleError($e, $this); } } - - private function stopContainers(array $containers, $server, int $timeout = 30) - { - if (empty($containers)) { - return; - } - $containerNames = []; - foreach ($containers as $container) { - $containerNames[] = str_replace('/', '', $container['Names']); - } - - $containerList = implode(' ', array_map('escapeshellarg', $containerNames)); - $commands = [ - "docker stop --time=$timeout $containerList", - "docker rm -f $containerList", - ]; - - instant_remote_process( - command: $commands, - server: $server, - throwError: false - ); - } } diff --git a/app/Livewire/Project/Application/PreviewsCompose.php b/app/Livewire/Project/Application/PreviewsCompose.php index b3e838bb3..334d96cad 100644 --- a/app/Livewire/Project/Application/PreviewsCompose.php +++ b/app/Livewire/Project/Application/PreviewsCompose.php @@ -38,9 +38,25 @@ public function generate() $domain = $domains->first(function ($_, $key) { return $key === $this->serviceName; }); - if ($domain) { - $domain = data_get($domain, 'domain'); - $url = Url::fromString($domain); + + $domain_string = data_get($domain, 'domain'); + + // If no domain is set in the main application, generate a default domain + if (empty($domain_string)) { + $server = $this->preview->application->destination->server; + $template = $this->preview->application->preview_url_template; + $random = new Cuid2; + + // Generate a unique domain like main app services do + $generated_fqdn = generateFqdn($server, $random); + + $preview_fqdn = str_replace('{{random}}', $random, $template); + $preview_fqdn = str_replace('{{domain}}', str($generated_fqdn)->after('://'), $preview_fqdn); + $preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn); + $preview_fqdn = str($generated_fqdn)->before('://').'://'.$preview_fqdn; + } else { + // Use the existing domain from the main application + $url = Url::fromString($domain_string); $template = $this->preview->application->preview_url_template; $host = $url->getHost(); $schema = $url->getScheme(); @@ -49,12 +65,15 @@ public function generate() $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn); $preview_fqdn = "$schema://$preview_fqdn"; - $docker_compose_domains = data_get($this->preview, 'docker_compose_domains'); - $docker_compose_domains = json_decode($docker_compose_domains, true); - $docker_compose_domains[$this->serviceName]['domain'] = $this->service->domain = $preview_fqdn; - $this->preview->docker_compose_domains = json_encode($docker_compose_domains); - $this->preview->save(); } + + // Save the generated domain + $docker_compose_domains = data_get($this->preview, 'docker_compose_domains'); + $docker_compose_domains = json_decode($docker_compose_domains, true); + $docker_compose_domains[$this->serviceName]['domain'] = $this->service->domain = $preview_fqdn; + $this->preview->docker_compose_domains = json_encode($docker_compose_domains); + $this->preview->save(); + $this->dispatch('update_links'); $this->dispatch('success', 'Domain generated.'); } diff --git a/app/Livewire/Project/Application/Source.php b/app/Livewire/Project/Application/Source.php index e27a550c1..932a302ad 100644 --- a/app/Livewire/Project/Application/Source.php +++ b/app/Livewire/Project/Application/Source.php @@ -111,8 +111,19 @@ public function changeSource($sourceId, $sourceType) $this->application->update([ 'source_id' => $sourceId, 'source_type' => $sourceType, - 'repository_project_id' => null, ]); + + ['repository' => $customRepository] = $this->application->customRepository(); + $repository = githubApi($this->application->source, "repos/{$customRepository}"); + $data = data_get($repository, 'data'); + $repository_project_id = data_get($data, 'id'); + if (isset($repository_project_id)) { + if ($this->application->repository_project_id !== $repository_project_id) { + $this->application->repository_project_id = $repository_project_id; + $this->application->save(); + } + } + $this->application->refresh(); $this->getSources(); $this->dispatch('success', 'Source updated!'); diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php index fefaff4f2..a5d80a11a 100644 --- a/app/Livewire/Project/CloneMe.php +++ b/app/Livewire/Project/CloneMe.php @@ -56,7 +56,6 @@ public function mount($project_uuid) $this->project_id = $this->project->id; $this->servers = currentTeam() ->servers() - ->with('destinations') ->get() ->reject(fn ($server) => $server->isBuildServer()); $this->newName = str($this->project->name.'-clone-'.(string) new Cuid2)->slug(); @@ -455,7 +454,7 @@ public function clone(string $type) if ($this->cloneVolumeData) { try { - StopService::dispatch($application, false, false); + StopService::dispatch($application); $sourceVolume = $volume->name; $targetVolume = $newPersistentVolume->name; $sourceServer = $application->service->destination->server; @@ -509,7 +508,7 @@ public function clone(string $type) if ($this->cloneVolumeData) { try { - StopService::dispatch($database->service, false, false); + StopService::dispatch($database->service); $sourceVolume = $volume->name; $targetVolume = $newPersistentVolume->name; $sourceServer = $database->service->destination->server; diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index 0d363e983..abc88d736 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -73,6 +73,9 @@ class BackupEdit extends Component #[Validate(['required', 'boolean'])] public bool $dumpAll = false; + #[Validate(['required', 'int', 'min:1', 'max:36000'])] + public int $timeout = 3600; + public function mount() { try { @@ -98,6 +101,7 @@ public function syncData(bool $toModel = false) $this->backup->s3_storage_id = $this->s3StorageId; $this->backup->databases_to_backup = $this->databasesToBackup; $this->backup->dump_all = $this->dumpAll; + $this->backup->timeout = $this->timeout; $this->customValidate(); $this->backup->save(); } else { @@ -114,6 +118,7 @@ public function syncData(bool $toModel = false) $this->s3StorageId = $this->backup->s3_storage_id; $this->databasesToBackup = $this->backup->databases_to_backup; $this->dumpAll = $this->backup->dump_all; + $this->timeout = $this->backup->timeout; } } diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php index 3fc721fda..2f3aae8cf 100644 --- a/app/Livewire/Project/Database/BackupExecutions.php +++ b/app/Livewire/Project/Database/BackupExecutions.php @@ -4,6 +4,7 @@ use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Livewire\Component; @@ -14,7 +15,19 @@ class BackupExecutions extends Component public $database; - public $executions = []; + public ?Collection $executions; + + public int $executions_count = 0; + + public int $skip = 0; + + public int $defaultTake = 10; + + public bool $showNext = false; + + public bool $showPrev = false; + + public int $currentPage = 1; public $setDeletableBackup; @@ -40,6 +53,20 @@ public function cleanupFailed() } } + public function cleanupDeleted() + { + if ($this->backup) { + $deletedCount = $this->backup->executions()->where('local_storage_deleted', true)->count(); + if ($deletedCount > 0) { + $this->backup->executions()->where('local_storage_deleted', true)->delete(); + $this->refreshBackupExecutions(); + $this->dispatch('success', "Cleaned up {$deletedCount} backup entries deleted from local storage."); + } else { + $this->dispatch('info', 'No backup entries found that are deleted from local storage.'); + } + } + } + public function deleteBackup($executionId, $password) { if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { @@ -85,18 +112,74 @@ public function download_file($exeuctionId) public function refreshBackupExecutions(): void { - if ($this->backup && $this->backup->exists) { - $this->executions = $this->backup->executions()->get()->toArray(); - } else { - $this->executions = []; + $this->loadExecutions(); + } + + public function reloadExecutions() + { + $this->loadExecutions(); + } + + public function previousPage(?int $take = null) + { + if ($take) { + $this->skip = $this->skip - $take; } + $this->skip = $this->skip - $this->defaultTake; + if ($this->skip < 0) { + $this->showPrev = false; + $this->skip = 0; + } + $this->updateCurrentPage(); + $this->loadExecutions(); + } + + public function nextPage(?int $take = null) + { + if ($take) { + $this->skip = $this->skip + $take; + } + $this->showPrev = true; + $this->updateCurrentPage(); + $this->loadExecutions(); + } + + private function loadExecutions() + { + if ($this->backup && $this->backup->exists) { + ['executions' => $executions, 'count' => $count] = $this->backup->executionsPaginated($this->skip, $this->defaultTake); + $this->executions = $executions; + $this->executions_count = $count; + } else { + $this->executions = collect([]); + $this->executions_count = 0; + } + $this->showMore(); + } + + private function showMore() + { + if ($this->executions->count() !== 0) { + $this->showNext = true; + if ($this->executions->count() < $this->defaultTake) { + $this->showNext = false; + } + + return; + } + } + + private function updateCurrentPage() + { + $this->currentPage = intval($this->skip / $this->defaultTake) + 1; } public function mount(ScheduledDatabaseBackup $backup) { $this->backup = $backup; $this->database = $backup->database; - $this->refreshBackupExecutions(); + $this->updateCurrentPage(); + $this->loadExecutions(); } public function server() @@ -121,8 +204,8 @@ public function render() { return view('livewire.project.database.backup-executions', [ 'checkboxes' => [ - ['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'], - // ['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'], + ['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently from S3 Storage'], + // ['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently from SFTP Storage'], ], ]); } diff --git a/app/Livewire/Project/Service/Heading.php b/app/Livewire/Project/Service/Heading.php index eb7f469a8..3492da324 100644 --- a/app/Livewire/Project/Service/Heading.php +++ b/app/Livewire/Project/Service/Heading.php @@ -6,7 +6,6 @@ use App\Actions\Service\StartService; use App\Actions\Service\StopService; use App\Enums\ProcessStatus; -use App\Events\ServiceStatusChanged; use App\Models\Service; use Illuminate\Support\Facades\Auth; use Livewire\Component; @@ -64,7 +63,7 @@ public function serviceChecked() $this->service->databases->each(function ($database) { $database->refresh(); }); - if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) { + if (is_null($this->service->config_hash)) { $this->service->isConfigurationChanged(true); } $this->dispatch('configurationChanged'); @@ -96,7 +95,7 @@ public function checkDeployments() public function start() { $activity = StartService::run($this->service, pullLatestImages: true); - $this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class); + $this->dispatch('activityMonitor', $activity->id); } public function forceDeploy() @@ -112,7 +111,7 @@ public function forceDeploy() $activity->save(); } $activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true); - $this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class); + $this->dispatch('activityMonitor', $activity->id); } catch (\Exception $e) { $this->dispatch('error', $e->getMessage()); } @@ -136,7 +135,7 @@ public function restart() return; } $activity = StartService::run($this->service, stopBeforeStart: true); - $this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class); + $this->dispatch('activityMonitor', $activity->id); } public function pullAndRestartEvent() @@ -148,7 +147,7 @@ public function pullAndRestartEvent() return; } $activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true); - $this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class); + $this->dispatch('activityMonitor', $activity->id); } public function render() diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php index 7da48f9fb..94a4c161c 100644 --- a/app/Livewire/Project/Shared/Danger.php +++ b/app/Livewire/Project/Shared/Danger.php @@ -99,10 +99,10 @@ public function delete($password) $this->resource->delete(); DeleteResourceJob::dispatch( $this->resource, - $this->delete_configurations, $this->delete_volumes, - $this->docker_cleanup, - $this->delete_connected_networks + $this->delete_connected_networks, + $this->delete_configurations, + $this->docker_cleanup ); return redirect()->route('project.resource.index', [ diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index 535ac6c67..966d626b1 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -170,6 +170,7 @@ public function submit() $this->syncData(true); $this->dispatch('success', 'Environment variable updated.'); $this->dispatch('envsUpdated'); + $this->dispatch('configurationChanged'); } catch (\Exception $e) { return handleError($e); } diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index ca1597d4f..2d55807c7 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -137,6 +137,13 @@ public function loadContainers() } } + public function updatedSelectedContainer() + { + if ($this->selected_container !== 'default') { + $this->connectToContainer(); + } + } + #[On('connectToServer')] public function connectToServer() { @@ -151,6 +158,9 @@ public function connectToServer() data_get($server, 'name'), data_get($server, 'uuid') ); + + // Dispatch a frontend event to ensure terminal gets focus after connection + $this->dispatch('terminal-should-focus'); } catch (\Throwable $e) { return handleError($e, $this); } finally { @@ -206,6 +216,9 @@ public function connectToContainer() data_get($container, 'container.Names'), data_get($container, 'server.uuid') ); + + // Dispatch a frontend event to ensure terminal gets focus after connection + $this->dispatch('terminal-should-focus'); } catch (\Throwable $e) { return handleError($e, $this); } finally { diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index fb19acb55..c8916bf19 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -412,7 +412,7 @@ public function cloneTo($destination_id) if ($this->cloneVolumeData) { try { - StopService::dispatch($application, false, false); + StopService::dispatch($application); $sourceVolume = $volume->name; $targetVolume = $newPersistentVolume->name; $sourceServer = $application->service->destination->server; @@ -454,7 +454,7 @@ public function cloneTo($destination_id) if ($this->cloneVolumeData) { try { - StopService::dispatch($database->service, false, false); + StopService::dispatch($database->service); $sourceVolume = $volume->name; $targetVolume = $newPersistentVolume->name; $sourceServer = $database->service->destination->server; diff --git a/app/Livewire/Project/Shared/Terminal.php b/app/Livewire/Project/Shared/Terminal.php index 819d364e2..de2deeed4 100644 --- a/app/Livewire/Project/Shared/Terminal.php +++ b/app/Livewire/Project/Shared/Terminal.php @@ -68,11 +68,16 @@ public function sendTerminalCommand($isContainer, $identifier, $serverUuid) // Escape the identifier for shell usage $escapedIdentifier = escapeshellarg($identifier); - $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$escapedIdentifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); + $shellCommand = 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && '. + 'if [ -f ~/.profile ]; then . ~/.profile; fi && '. + 'if [ -n "$SHELL" ] && [ -x "$SHELL" ]; then exec $SHELL; else sh; fi'; + $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$escapedIdentifier} sh -c '{$shellCommand}'"); } else { - $command = SshMultiplexingHelper::generateSshCommand($server, 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n "$SHELL" ]; then exec $SHELL; else sh; fi'); + $shellCommand = 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && '. + 'if [ -f ~/.profile ]; then . ~/.profile; fi && '. + 'if [ -n "$SHELL" ] && [ -x "$SHELL" ]; then exec $SHELL; else sh; fi'; + $command = SshMultiplexingHelper::generateSshCommand($server, $shellCommand); } - // ssh command is sent back to frontend then to websocket // this is done because the websocket connection is not available here // a better solution would be to remove websocket on NodeJS and work with something like diff --git a/app/Livewire/Server/DockerCleanup.php b/app/Livewire/Server/DockerCleanup.php index d3378d63f..c97a8f2c9 100644 --- a/app/Livewire/Server/DockerCleanup.php +++ b/app/Livewire/Server/DockerCleanup.php @@ -71,7 +71,7 @@ public function instantSave() public function manualCleanup() { try { - DockerCleanupJob::dispatch($this->server, true); + DockerCleanupJob::dispatch($this->server, true, $this->deleteUnusedVolumes, $this->deleteUnusedNetworks); $this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.'); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index 9332c2517..1cf8c839e 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -19,7 +19,15 @@ class Proxy extends Component public ?string $redirect_url = null; - protected $listeners = ['saveConfiguration' => 'submit']; + public function getListeners() + { + $teamId = auth()->user()->currentTeam()->id; + + return [ + 'saveConfiguration' => 'submit', + "echo-private:team.{$teamId},ProxyStatusChangedUI" => '$refresh', + ]; + } protected $rules = [ 'server.settings.generate_exact_labels' => 'required|boolean', diff --git a/app/Livewire/Server/Security/Patches.php b/app/Livewire/Server/Security/Patches.php index 9392fc351..b7d17a61d 100644 --- a/app/Livewire/Server/Security/Patches.php +++ b/app/Livewire/Server/Security/Patches.php @@ -69,6 +69,7 @@ public function updateAllPackages() { if (! $this->packageManager || ! $this->osId) { $this->dispatch('error', message: 'Run “Check for updates” first.'); + return; } diff --git a/app/Livewire/Settings/Advanced.php b/app/Livewire/Settings/Advanced.php new file mode 100644 index 000000000..4425b414d --- /dev/null +++ b/app/Livewire/Settings/Advanced.php @@ -0,0 +1,118 @@ +route('dashboard'); + } + $this->server = Server::findOrFail(0); + $this->settings = instanceSettings(); + $this->custom_dns_servers = $this->settings->custom_dns_servers; + $this->allowed_ips = $this->settings->allowed_ips; + $this->do_not_track = $this->settings->do_not_track; + $this->is_registration_enabled = $this->settings->is_registration_enabled; + $this->is_dns_validation_enabled = $this->settings->is_dns_validation_enabled; + $this->is_api_enabled = $this->settings->is_api_enabled; + $this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation; + $this->is_sponsorship_popup_enabled = $this->settings->is_sponsorship_popup_enabled; + } + + public function submit() + { + try { + $this->validate(); + + $this->custom_dns_servers = str($this->custom_dns_servers)->replaceEnd(',', '')->trim(); + $this->custom_dns_servers = str($this->custom_dns_servers)->trim()->explode(',')->map(function ($dns) { + return str($dns)->trim()->lower(); + })->unique()->implode(','); + + $this->allowed_ips = str($this->allowed_ips)->replaceEnd(',', '')->trim(); + $this->allowed_ips = str($this->allowed_ips)->trim()->explode(',')->map(function ($ip) { + return str($ip)->trim(); + })->unique()->implode(','); + + $this->instantSave(); + } catch (\Exception $e) { + return handleError($e, $this); + } + } + + public function instantSave() + { + try { + $this->settings->is_registration_enabled = $this->is_registration_enabled; + $this->settings->do_not_track = $this->do_not_track; + $this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled; + $this->settings->custom_dns_servers = $this->custom_dns_servers; + $this->settings->is_api_enabled = $this->is_api_enabled; + $this->settings->allowed_ips = $this->allowed_ips; + $this->settings->is_sponsorship_popup_enabled = $this->is_sponsorship_popup_enabled; + $this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation; + $this->settings->save(); + $this->dispatch('success', 'Settings updated!'); + } catch (\Exception $e) { + return handleError($e, $this); + } + } + + public function toggleTwoStepConfirmation($password): bool + { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); + + return false; + } + + $this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation = true; + $this->settings->save(); + $this->dispatch('success', 'Two step confirmation has been disabled.'); + + return true; + } + + public function render() + { + return view('livewire.settings.advanced'); + } +} diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php index 3d90024b7..bce343224 100644 --- a/app/Livewire/Settings/Index.php +++ b/app/Livewire/Settings/Index.php @@ -2,11 +2,8 @@ namespace App\Livewire\Settings; -use App\Jobs\CheckForUpdatesJob; use App\Models\InstanceSettings; use App\Models\Server; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Hash; use Livewire\Attributes\Computed; use Livewire\Attributes\Validate; use Livewire\Component; @@ -15,10 +12,7 @@ class Index extends Component { public InstanceSettings $settings; - protected Server $server; - - #[Validate('boolean')] - public bool $is_auto_update_enabled; + public Server $server; #[Validate('nullable|string|max:255')] public ?string $fqdn = null; @@ -29,45 +23,18 @@ class Index extends Component #[Validate('required|integer|min:1025|max:65535')] public int $public_port_max; - #[Validate('nullable|string')] - public ?string $custom_dns_servers = null; - #[Validate('nullable|string|max:255')] public ?string $instance_name = null; - #[Validate('nullable|string')] - public ?string $allowed_ips = null; - #[Validate('nullable|string')] public ?string $public_ipv4 = null; #[Validate('nullable|string')] public ?string $public_ipv6 = null; - #[Validate('string')] - public string $auto_update_frequency; - - #[Validate('string|required')] - public string $update_check_frequency; - #[Validate('required|string|timezone')] public string $instance_timezone; - #[Validate('boolean')] - public bool $do_not_track; - - #[Validate('boolean')] - public bool $is_registration_enabled; - - #[Validate('boolean')] - public bool $is_dns_validation_enabled; - - #[Validate('boolean')] - public bool $is_api_enabled; - - #[Validate('boolean')] - public bool $disable_two_step_confirmation; - public function render() { return view('livewire.settings.index'); @@ -77,26 +44,16 @@ public function mount() { if (! isInstanceAdmin()) { return redirect()->route('dashboard'); - } else { - $this->settings = instanceSettings(); - $this->fqdn = $this->settings->fqdn; - $this->public_port_min = $this->settings->public_port_min; - $this->public_port_max = $this->settings->public_port_max; - $this->custom_dns_servers = $this->settings->custom_dns_servers; - $this->instance_name = $this->settings->instance_name; - $this->allowed_ips = $this->settings->allowed_ips; - $this->public_ipv4 = $this->settings->public_ipv4; - $this->public_ipv6 = $this->settings->public_ipv6; - $this->do_not_track = $this->settings->do_not_track; - $this->is_auto_update_enabled = $this->settings->is_auto_update_enabled; - $this->is_registration_enabled = $this->settings->is_registration_enabled; - $this->is_dns_validation_enabled = $this->settings->is_dns_validation_enabled; - $this->is_api_enabled = $this->settings->is_api_enabled; - $this->auto_update_frequency = $this->settings->auto_update_frequency; - $this->update_check_frequency = $this->settings->update_check_frequency; - $this->instance_timezone = $this->settings->instance_timezone; - $this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation; } + $this->settings = instanceSettings(); + $this->server = Server::findOrFail(0); + $this->fqdn = $this->settings->fqdn; + $this->public_port_min = $this->settings->public_port_min; + $this->public_port_max = $this->settings->public_port_max; + $this->instance_name = $this->settings->instance_name; + $this->public_ipv4 = $this->settings->public_ipv4; + $this->public_ipv6 = $this->settings->public_ipv6; + $this->instance_timezone = $this->settings->instance_timezone; } #[Computed] @@ -111,28 +68,12 @@ public function timezones(): array public function instantSave($isSave = true) { $this->validate(); - if ($this->settings->is_auto_update_enabled === true) { - $this->validate([ - 'auto_update_frequency' => ['required', 'string'], - ]); - } - $this->settings->fqdn = $this->fqdn; $this->settings->public_port_min = $this->public_port_min; $this->settings->public_port_max = $this->public_port_max; - $this->settings->custom_dns_servers = $this->custom_dns_servers; $this->settings->instance_name = $this->instance_name; - $this->settings->allowed_ips = $this->allowed_ips; $this->settings->public_ipv4 = $this->public_ipv4; $this->settings->public_ipv6 = $this->public_ipv6; - $this->settings->do_not_track = $this->do_not_track; - $this->settings->is_auto_update_enabled = $this->is_auto_update_enabled; - $this->settings->is_registration_enabled = $this->is_registration_enabled; - $this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled; - $this->settings->is_api_enabled = $this->is_api_enabled; - $this->settings->auto_update_frequency = $this->auto_update_frequency; - $this->settings->update_check_frequency = $this->update_check_frequency; - $this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation; $this->settings->instance_timezone = $this->instance_timezone; if ($isSave) { $this->settings->save(); @@ -144,7 +85,6 @@ public function submit() { try { $error_show = false; - $this->server = Server::findOrFail(0); $this->resetErrorBag(); if (! validate_timezone($this->instance_timezone)) { @@ -161,46 +101,15 @@ public function submit() } $this->validate(); - if ($this->is_auto_update_enabled && ! validate_cron_expression($this->auto_update_frequency)) { - $this->dispatch('error', 'Invalid Cron / Human expression for Auto Update Frequency.'); - if (empty($this->auto_update_frequency)) { - $this->auto_update_frequency = '0 0 * * *'; - } - - return; - } - - if (! validate_cron_expression($this->update_check_frequency)) { - $this->dispatch('error', 'Invalid Cron / Human expression for Update Check Frequency.'); - if (empty($this->update_check_frequency)) { - $this->update_check_frequency = '0 * * * *'; - } - - return; - } - - if ($this->settings->is_dns_validation_enabled && $this->settings->fqdn) { - if (! validate_dns_entry($this->settings->fqdn, $this->server)) { - $this->dispatch('error', "Validating DNS failed.

Make sure you have added the DNS records correctly.

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

Check this documentation for further help."); + if ($this->settings->is_dns_validation_enabled && $this->fqdn) { + if (! validate_dns_entry($this->fqdn, $this->server)) { + $this->dispatch('error', "Validating DNS failed.

Make sure you have added the DNS records correctly.

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

Check this documentation for further help."); $error_show = true; } } - if ($this->settings->fqdn) { - check_domain_usage(domain: $this->settings->fqdn); + if ($this->fqdn) { + check_domain_usage(domain: $this->fqdn); } - $this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->replaceEnd(',', '')->trim(); - $this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->trim()->explode(',')->map(function ($dns) { - return str($dns)->trim()->lower(); - }); - $this->settings->custom_dns_servers = $this->settings->custom_dns_servers->unique(); - $this->settings->custom_dns_servers = $this->settings->custom_dns_servers->implode(','); - - $this->settings->allowed_ips = str($this->settings->allowed_ips)->replaceEnd(',', '')->trim(); - $this->settings->allowed_ips = str($this->settings->allowed_ips)->trim()->explode(',')->map(function ($ip) { - return str($ip)->trim(); - }); - $this->settings->allowed_ips = $this->settings->allowed_ips->unique(); - $this->settings->allowed_ips = $this->settings->allowed_ips->implode(','); $this->instantSave(isSave: false); @@ -213,31 +122,4 @@ public function submit() return handleError($e, $this); } } - - public function checkManually() - { - CheckForUpdatesJob::dispatchSync(); - $this->dispatch('updateAvailable'); - $settings = instanceSettings(); - if ($settings->new_version_available) { - $this->dispatch('success', 'New version available!'); - } else { - $this->dispatch('success', 'No new version available.'); - } - } - - public function toggleTwoStepConfirmation($password): bool - { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); - - return false; - } - - $this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation = true; - $this->settings->save(); - $this->dispatch('success', 'Two step confirmation has been disabled.'); - - return true; - } } diff --git a/app/Livewire/Settings/Updates.php b/app/Livewire/Settings/Updates.php new file mode 100644 index 000000000..fe20763b6 --- /dev/null +++ b/app/Livewire/Settings/Updates.php @@ -0,0 +1,101 @@ +server = Server::findOrFail(0); + + $this->settings = instanceSettings(); + $this->auto_update_frequency = $this->settings->auto_update_frequency; + $this->update_check_frequency = $this->settings->update_check_frequency; + $this->is_auto_update_enabled = $this->settings->is_auto_update_enabled; + } + + public function instantSave() + { + try { + if ($this->settings->is_auto_update_enabled === true) { + $this->validate([ + 'auto_update_frequency' => ['required', 'string'], + ]); + } + $this->settings->auto_update_frequency = $this->auto_update_frequency; + $this->settings->update_check_frequency = $this->update_check_frequency; + $this->settings->is_auto_update_enabled = $this->is_auto_update_enabled; + $this->settings->save(); + $this->dispatch('success', 'Settings updated!'); + } catch (\Exception $e) { + return handleError($e, $this); + } + } + + public function submit() + { + try { + $this->resetErrorBag(); + $this->validate(); + + if ($this->is_auto_update_enabled && ! validate_cron_expression($this->auto_update_frequency)) { + $this->dispatch('error', 'Invalid Cron / Human expression for Auto Update Frequency.'); + if (empty($this->auto_update_frequency)) { + $this->auto_update_frequency = '0 0 * * *'; + } + + return; + } + + if (! validate_cron_expression($this->update_check_frequency)) { + $this->dispatch('error', 'Invalid Cron / Human expression for Update Check Frequency.'); + if (empty($this->update_check_frequency)) { + $this->update_check_frequency = '0 * * * *'; + } + + return; + } + + $this->instantSave(); + $this->server->setupDynamicProxyConfiguration(); + } catch (\Exception $e) { + return handleError($e, $this); + } + } + + public function checkManually() + { + CheckForUpdatesJob::dispatchSync(); + $this->dispatch('updateAvailable'); + $settings = instanceSettings(); + if ($settings->new_version_available) { + $this->dispatch('success', 'New version available!'); + } else { + $this->dispatch('success', 'No new version available.'); + } + } + + public function render() + { + return view('livewire.settings.updates'); + } +} diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php index bb5ed0aa8..57cb79fca 100644 --- a/app/Livewire/SettingsBackup.php +++ b/app/Livewire/SettingsBackup.php @@ -46,32 +46,31 @@ public function mount() { if (! isInstanceAdmin()) { return redirect()->route('dashboard'); - } else { - $settings = instanceSettings(); - $this->server = Server::findOrFail(0); - $this->database = StandalonePostgresql::whereName('coolify-db')->first(); - $s3s = S3Storage::whereTeamId(0)->get() ?? []; - if ($this->database) { - $this->uuid = $this->database->uuid; - $this->name = $this->database->name; - $this->description = $this->database->description; - $this->postgres_user = $this->database->postgres_user; - $this->postgres_password = $this->database->postgres_password; - - if ($this->database->status !== 'running') { - $this->database->status = 'running'; - $this->database->save(); - } - $this->backup = $this->database->scheduledBackups->first(); - if ($this->backup && ! $this->server->isFunctional()) { - $this->backup->enabled = false; - $this->backup->save(); - } - $this->executions = $this->backup->executions; - } - $this->settings = $settings; - $this->s3s = $s3s; } + $settings = instanceSettings(); + $this->server = Server::findOrFail(0); + $this->database = StandalonePostgresql::whereName('coolify-db')->first(); + $s3s = S3Storage::whereTeamId(0)->get() ?? []; + if ($this->database) { + $this->uuid = $this->database->uuid; + $this->name = $this->database->name; + $this->description = $this->database->description; + $this->postgres_user = $this->database->postgres_user; + $this->postgres_password = $this->database->postgres_password; + + if ($this->database->status !== 'running') { + $this->database->status = 'running'; + $this->database->save(); + } + $this->backup = $this->database->scheduledBackups->first(); + if ($this->backup && ! $this->server->isFunctional()) { + $this->backup->enabled = false; + $this->backup->save(); + } + $this->executions = $this->backup->executions; + } + $this->settings = $settings; + $this->s3s = $s3s; } public function addCoolifyDatabase() diff --git a/app/Livewire/Storage/Form.php b/app/Livewire/Storage/Form.php index 8ca0020c7..ad1627863 100644 --- a/app/Livewire/Storage/Form.php +++ b/app/Livewire/Storage/Form.php @@ -31,7 +31,7 @@ class Form extends Component 'storage.endpoint' => 'Endpoint', ]; - public function test_s3_connection() + public function testConnection() { try { $this->storage->testConnection(shouldSave: true); @@ -45,6 +45,8 @@ public function test_s3_connection() public function delete() { try { + $this->authorize('delete', $this->storage); + $this->storage->delete(); return redirect()->route('storage.index'); @@ -57,7 +59,7 @@ public function submit() { $this->validate(); try { - $this->test_s3_connection(); + $this->testConnection(); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php index cfb47d9d8..6d6915ae2 100644 --- a/app/Livewire/Team/AdminView.php +++ b/app/Livewire/Team/AdminView.php @@ -3,7 +3,6 @@ namespace App\Livewire\Team; use App\Models\InstanceSettings; -use App\Models\Team; use App\Models\User; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; @@ -53,30 +52,12 @@ public function getUsers() } } - private function finalizeDeletion(User $user, Team $team) - { - $servers = $team->servers; - foreach ($servers as $server) { - $resources = $server->definedResources(); - foreach ($resources as $resource) { - $resource->forceDelete(); - } - $server->forceDelete(); - } - - $projects = $team->projects; - foreach ($projects as $project) { - $project->forceDelete(); - } - $team->members()->detach($user->id); - $team->delete(); - } - public function delete($id, $password) { if (! isInstanceAdmin()) { return redirect()->route('dashboard'); } + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); @@ -84,52 +65,22 @@ public function delete($id, $password) return; } } + if (! auth()->user()->isInstanceAdmin()) { return $this->dispatch('error', 'You are not authorized to delete users'); } + $user = User::find($id); - $teams = $user->teams; - foreach ($teams as $team) { - $user_alone_in_team = $team->members->count() === 1; - if ($team->id === 0) { - if ($user_alone_in_team) { - return $this->dispatch('error', 'User is alone in the root team, cannot delete'); - } - } - if ($user_alone_in_team) { - $this->finalizeDeletion($user, $team); - - continue; - } - if ($user->isOwner()) { - $found_other_owner_or_admin = $team->members->filter(function ($member) { - return $member->pivot->role === 'owner' || $member->pivot->role === 'admin'; - })->where('id', '!=', $user->id)->first(); - - if ($found_other_owner_or_admin) { - $team->members()->detach($user->id); - - continue; - } else { - $found_other_member_who_is_not_owner = $team->members->filter(function ($member) { - return $member->pivot->role === 'member'; - })->first(); - if ($found_other_member_who_is_not_owner) { - $found_other_member_who_is_not_owner->pivot->role = 'owner'; - $found_other_member_who_is_not_owner->pivot->save(); - $team->members()->detach($user->id); - } else { - $this->finalizeDeletion($user, $team); - } - - continue; - } - } else { - $team->members()->detach($user->id); - } + if (! $user) { + return $this->dispatch('error', 'User not found'); + } + + try { + $user->delete(); + $this->getUsers(); + } catch (\Exception $e) { + return $this->dispatch('error', $e->getMessage()); } - $user->delete(); - $this->getUsers(); } public function render() diff --git a/app/Livewire/Team/Invitations.php b/app/Livewire/Team/Invitations.php index 93432efc8..3af0e0e92 100644 --- a/app/Livewire/Team/Invitations.php +++ b/app/Livewire/Team/Invitations.php @@ -3,6 +3,7 @@ namespace App\Livewire\Team; use App\Models\TeamInvitation; +use App\Models\User; use Livewire\Component; class Invitations extends Component @@ -14,8 +15,13 @@ class Invitations extends Component public function deleteInvitation(int $invitation_id) { try { - $initiation_found = TeamInvitation::ownedByCurrentTeam()->findOrFail($invitation_id); - $initiation_found->delete(); + $invitation = TeamInvitation::ownedByCurrentTeam()->findOrFail($invitation_id); + $user = User::whereEmail($invitation->email)->first(); + if (filled($user)) { + $user->deleteIfNotVerifiedAndForcePasswordReset(); + } + + $invitation->delete(); $this->refreshInvitations(); $this->dispatch('success', 'Invitation revoked.'); } catch (\Exception) { diff --git a/app/Livewire/Team/InviteLink.php b/app/Livewire/Team/InviteLink.php index 25f8a1ff5..fb0c51e54 100644 --- a/app/Livewire/Team/InviteLink.php +++ b/app/Livewire/Team/InviteLink.php @@ -29,15 +29,15 @@ public function mount() public function viaEmail() { - $this->generate_invite_link(sendEmail: true); + $this->generateInviteLink(sendEmail: true); } public function viaLink() { - $this->generate_invite_link(sendEmail: false); + $this->generateInviteLink(sendEmail: false); } - private function generate_invite_link(bool $sendEmail = false) + private function generateInviteLink(bool $sendEmail = false) { try { $this->validate(); diff --git a/app/Models/Application.php b/app/Models/Application.php index f3f063d19..86eea1de8 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -798,7 +798,7 @@ public function environment() public function previews() { - return $this->hasMany(ApplicationPreview::class); + return $this->hasMany(ApplicationPreview::class)->orderBy('pull_request_id', 'desc'); } public function deployment_queue() @@ -836,9 +836,14 @@ public function get_last_days_deployments() return ApplicationDeploymentQueue::where('application_id', $this->id)->where('created_at', '>=', now()->subDays(7))->orderBy('created_at', 'desc')->get(); } - public function deployments(int $skip = 0, int $take = 10) + public function deployments(int $skip = 0, int $take = 10, ?string $pullRequestId = null) { $deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->orderBy('created_at', 'desc'); + + if ($pullRequestId) { + $deployments = $deployments->where('pull_request_id', $pullRequestId); + } + $count = $deployments->count(); $deployments = $deployments->skip($skip)->take($take)->get(); @@ -1578,34 +1583,6 @@ public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false } } - public function generate_preview_fqdn(int $pull_request_id) - { - $preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->id, $pull_request_id); - if (is_null(data_get($preview, 'fqdn')) && $this->fqdn) { - if (str($this->fqdn)->contains(',')) { - $url = Url::fromString(str($this->fqdn)->explode(',')[0]); - $preview_fqdn = getFqdnWithoutPort(str($this->fqdn)->explode(',')[0]); - } else { - $url = Url::fromString($this->fqdn); - if (data_get($preview, 'fqdn')) { - $preview_fqdn = getFqdnWithoutPort(data_get($preview, 'fqdn')); - } - } - $template = $this->preview_url_template; - $host = $url->getHost(); - $schema = $url->getScheme(); - $random = new Cuid2; - $preview_fqdn = str_replace('{{random}}', $random, $template); - $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); - $preview_fqdn = str_replace('{{pr_id}}', $pull_request_id, $preview_fqdn); - $preview_fqdn = "$schema://$preview_fqdn"; - $preview->fqdn = $preview_fqdn; - $preview->save(); - } - - return $preview; - } - public static function getDomainsByUuid(string $uuid): array { $application = self::where('uuid', $uuid)->first(); diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php index bf2bf05bf..f45f9da40 100644 --- a/app/Models/ApplicationPreview.php +++ b/app/Models/ApplicationPreview.php @@ -2,19 +2,25 @@ namespace App\Models; +use Illuminate\Database\Eloquent\SoftDeletes; use Spatie\Url\Url; use Visus\Cuid2\Cuid2; class ApplicationPreview extends BaseModel { + use SoftDeletes; + protected $guarded = []; protected static function booted() { - static::deleting(function ($preview) { + static::forceDeleting(function ($preview) { + $server = $preview->application->destination->server; + $application = $preview->application; + if (data_get($preview, 'application.build_pack') === 'dockercompose') { - $server = $preview->application->destination->server; - $composeFile = $preview->application->parse(pull_request_id: $preview->pull_request_id); + // Docker Compose volume and network cleanup + $composeFile = $application->parse(pull_request_id: $preview->pull_request_id); $volumes = data_get($composeFile, 'volumes'); $networks = data_get($composeFile, 'networks'); $networkKeys = collect($networks)->keys(); @@ -26,7 +32,18 @@ protected static function booted() instant_remote_process(["docker network disconnect $key coolify-proxy"], $server, false); instant_remote_process(["docker network rm $key"], $server, false); }); + } else { + // Regular application volume cleanup + $persistentStorages = $preview->persistentStorages()->get() ?? collect(); + if ($persistentStorages->count() > 0) { + foreach ($persistentStorages as $storage) { + instant_remote_process(["docker volume rm -f $storage->name"], $server, false); + } + } } + + // Clean up persistent storage records + $preview->persistentStorages()->delete(); }); static::saving(function ($preview) { if ($preview->isDirty('status')) { @@ -50,12 +67,23 @@ public function application() return $this->belongsTo(Application::class); } - public function generate_preview_fqdn_compose() + public function persistentStorages() { - $domains = collect(json_decode($this->application->docker_compose_domains)) ?? collect(); - foreach ($domains as $service_name => $domain) { - $domain = data_get($domain, 'domain'); - $url = Url::fromString($domain); + return $this->morphMany(\App\Models\LocalPersistentVolume::class, 'resource'); + } + + public function generate_preview_fqdn() + { + if (is_null($this->fqdn) && $this->application->fqdn) { + if (str($this->application->fqdn)->contains(',')) { + $url = Url::fromString(str($this->application->fqdn)->explode(',')[0]); + $preview_fqdn = getFqdnWithoutPort(str($this->application->fqdn)->explode(',')[0]); + } else { + $url = Url::fromString($this->application->fqdn); + if ($this->fqdn) { + $preview_fqdn = getFqdnWithoutPort($this->fqdn); + } + } $template = $this->application->preview_url_template; $host = $url->getHost(); $schema = $url->getScheme(); @@ -64,12 +92,76 @@ public function generate_preview_fqdn_compose() $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn); $preview_fqdn = "$schema://$preview_fqdn"; - $docker_compose_domains = data_get($this, 'docker_compose_domains'); - $docker_compose_domains = json_decode($docker_compose_domains, true); - $docker_compose_domains[$service_name]['domain'] = $preview_fqdn; - $docker_compose_domains = json_encode($docker_compose_domains); - $this->docker_compose_domains = $docker_compose_domains; + $this->fqdn = $preview_fqdn; $this->save(); } + + return $this; + } + + public function generate_preview_fqdn_compose() + { + $services = collect(json_decode($this->application->docker_compose_domains)) ?? collect(); + $docker_compose_domains = data_get($this, 'docker_compose_domains'); + $docker_compose_domains = json_decode($docker_compose_domains, true) ?? []; + + // Get all services from the parsed compose file to ensure all services have entries + $parsedServices = $this->application->parse(pull_request_id: $this->pull_request_id); + if (isset($parsedServices['services'])) { + foreach ($parsedServices['services'] as $serviceName => $service) { + if (! isDatabaseImage(data_get($service, 'image'))) { + // Remove PR suffix from service name to get original service name + $originalServiceName = str($serviceName)->replaceLast('-pr-'.$this->pull_request_id, '')->toString(); + + // Ensure all services have an entry, even if empty + if (! $services->has($originalServiceName)) { + $services->put($originalServiceName, ['domain' => '']); + } + } + } + } + + foreach ($services as $service_name => $service_config) { + $domain_string = data_get($service_config, 'domain'); + + // If domain string is empty or null, don't auto-generate domain + // Only generate domains when main app already has domains set + if (empty($domain_string)) { + // Ensure service has an empty domain entry for form binding + $docker_compose_domains[$service_name]['domain'] = ''; + + continue; + } + + $service_domains = str($domain_string)->explode(',')->map(fn ($d) => trim($d)); + + $preview_domains = []; + foreach ($service_domains as $domain) { + if (empty($domain)) { + continue; + } + + $url = Url::fromString($domain); + $template = $this->application->preview_url_template; + $host = $url->getHost(); + $schema = $url->getScheme(); + $random = new Cuid2; + $preview_fqdn = str_replace('{{random}}', $random, $template); + $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); + $preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn); + $preview_fqdn = "$schema://$preview_fqdn"; + $preview_domains[] = $preview_fqdn; + } + + if (! empty($preview_domains)) { + $docker_compose_domains[$service_name]['domain'] = implode(',', $preview_domains); + } else { + // Ensure service has an empty domain entry for form binding + $docker_compose_domains[$service_name]['domain'] = ''; + } + } + + $this->docker_compose_domains = json_encode($docker_compose_domains); + $this->save(); } } diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 04081fce0..b8bde5c84 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -118,7 +118,14 @@ public function realValue(): Attribute return null; } - return $this->get_real_environment_variables($this->value, $resource); + $real_value = $this->get_real_environment_variables($this->value, $resource); + if ($this->is_literal || $this->is_multiline) { + $real_value = '\''.$real_value.'\''; + } else { + $real_value = escapeEnvVariables($real_value); + } + + return $real_value; } ); } diff --git a/app/Models/OauthSetting.php b/app/Models/OauthSetting.php index bfd332c87..08e08d85b 100644 --- a/app/Models/OauthSetting.php +++ b/app/Models/OauthSetting.php @@ -27,6 +27,7 @@ public function couldBeEnabled(): bool case 'azure': return filled($this->client_id) && filled($this->client_secret) && filled($this->tenant); case 'authentik': + case 'clerk': return filled($this->client_id) && filled($this->client_secret) && filled($this->base_url); default: return filled($this->client_id) && filled($this->client_secret); diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php index 473fc7b4b..90204d8df 100644 --- a/app/Models/ScheduledDatabaseBackup.php +++ b/app/Models/ScheduledDatabaseBackup.php @@ -36,6 +36,18 @@ public function get_last_days_backup_status($days = 7) return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get(); } + public function executionsPaginated(int $skip = 0, int $take = 10) + { + $executions = $this->hasMany(ScheduledDatabaseBackupExecution::class)->orderBy('created_at', 'desc'); + $count = $executions->count(); + $executions = $executions->skip($skip)->take($take)->get(); + + return [ + 'count' => $count, + 'executions' => $executions, + ]; + } + public function server() { if ($this->database) { diff --git a/app/Models/Server.php b/app/Models/Server.php index eac5bbcc1..41ecdafb8 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -887,7 +887,7 @@ public function privateKey() public function muxFilename() { - return $this->uuid; + return 'mux_'.$this->uuid; } public function team() diff --git a/app/Models/Service.php b/app/Models/Service.php index c3c8f3215..da6c34fbb 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Enums\ProcessStatus; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -9,6 +10,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\Storage; use OpenApi\Attributes as OA; +use Spatie\Activitylog\Models\Activity; use Spatie\Url\Url; use Visus\Cuid2\Cuid2; @@ -116,6 +118,18 @@ public function isExited() return (bool) str($this->status)->contains('exited'); } + public function isStarting(): bool + { + try { + $activity = Activity::where('properties->type_uuid', $this->uuid)->latest()->first(); + $status = data_get($activity, 'properties.status'); + + return $status === ProcessStatus::QUEUED->value || $status === ProcessStatus::IN_PROGRESS->value; + } catch (\Throwable) { + return false; + } + } + public function type() { return 'service'; @@ -159,6 +173,10 @@ public function deleteConnectedNetworks() public function getStatusAttribute() { + if ($this->isStarting()) { + return 'starting:unhealthy'; + } + $applications = $this->applications; $databases = $this->databases; @@ -1242,26 +1260,17 @@ public function saveComposeConfigs() return 3; }); + $envs = collect([]); foreach ($sorted as $env) { - if (version_compare($env->version, '4.0.0-beta.347', '<=')) { - $commands[] = "echo '{$env->key}={$env->real_value}' >> .env"; - } else { - $real_value = $env->real_value; - if ($env->version === '4.0.0-beta.239') { - $real_value = $env->real_value; - } else { - if ($env->is_literal || $env->is_multiline) { - $real_value = '\''.$real_value.'\''; - } else { - $real_value = escapeEnvVariables($env->real_value); - } - } - $commands[] = "echo \"{$env->key}={$real_value}\" >> .env"; - } + $envs->push("{$env->key}={$env->real_value}"); } - if ($sorted->count() === 0) { + if ($envs->count() === 0) { $commands[] = 'touch .env'; + } else { + $envs_base64 = base64_encode($envs->implode("\n")); + $commands[] = "echo '$envs_base64' | base64 -d | tee .env > /dev/null"; } + instant_remote_process($commands, $this->server); } diff --git a/app/Models/TeamInvitation.php b/app/Models/TeamInvitation.php index bc1a90d58..0fea1806b 100644 --- a/app/Models/TeamInvitation.php +++ b/app/Models/TeamInvitation.php @@ -33,6 +33,10 @@ public function isValid() return true; } else { $this->delete(); + $user = User::whereEmail($this->email)->first(); + if (filled($user)) { + $user->deleteIfNotVerifiedAndForcePasswordReset(); + } return false; } diff --git a/app/Models/User.php b/app/Models/User.php index f9515ad09..6cd1b66db 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -72,6 +72,93 @@ protected static function boot() $new_team = Team::create($team); $user->teams()->attach($new_team, ['role' => 'owner']); }); + + static::deleting(function (User $user) { + \DB::transaction(function () use ($user) { + $teams = $user->teams; + foreach ($teams as $team) { + $user_alone_in_team = $team->members->count() === 1; + + // Prevent deletion if user is alone in root team + if ($team->id === 0 && $user_alone_in_team) { + throw new \Exception('User is alone in the root team, cannot delete'); + } + + if ($user_alone_in_team) { + static::finalizeTeamDeletion($user, $team); + // Delete any pending team invitations for this user + TeamInvitation::whereEmail($user->email)->delete(); + + continue; + } + + // Load the user's role for this team + $userRole = $team->members->where('id', $user->id)->first()?->pivot?->role; + + if ($userRole === 'owner') { + $found_other_owner_or_admin = $team->members->filter(function ($member) use ($user) { + return ($member->pivot->role === 'owner' || $member->pivot->role === 'admin') && $member->id !== $user->id; + })->first(); + + if ($found_other_owner_or_admin) { + $team->members()->detach($user->id); + + continue; + } else { + $found_other_member_who_is_not_owner = $team->members->filter(function ($member) { + return $member->pivot->role === 'member'; + })->first(); + + if ($found_other_member_who_is_not_owner) { + $found_other_member_who_is_not_owner->pivot->role = 'owner'; + $found_other_member_who_is_not_owner->pivot->save(); + $team->members()->detach($user->id); + } else { + static::finalizeTeamDeletion($user, $team); + } + + continue; + } + } else { + $team->members()->detach($user->id); + } + } + }); + }); + } + + /** + * Finalize team deletion by cleaning up all associated resources + */ + private static function finalizeTeamDeletion(User $user, Team $team) + { + $servers = $team->servers; + foreach ($servers as $server) { + $resources = $server->definedResources(); + foreach ($resources as $resource) { + $resource->forceDelete(); + } + $server->forceDelete(); + } + + $projects = $team->projects; + foreach ($projects as $project) { + $project->forceDelete(); + } + + $team->members()->detach($user->id); + $team->delete(); + } + + /** + * Delete the user if they are not verified and have a force password reset. + * This is used to clean up users that have been invited, did not accept the invitation (and did not verify their email and have a force password reset). + */ + public function deleteIfNotVerifiedAndForcePasswordReset() + { + if ($this->hasVerifiedEmail() === false && $this->force_password_reset === true) { + $this->delete(); + } } public function recreate_personal_team() diff --git a/app/Policies/S3StoragePolicy.php b/app/Policies/S3StoragePolicy.php new file mode 100644 index 000000000..28f5f8426 --- /dev/null +++ b/app/Policies/S3StoragePolicy.php @@ -0,0 +1,66 @@ +teams()->where('id', $storage->team_id)->exists(); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return true; + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, Server $server): bool + { + return $user->teams()->get()->firstWhere('id', $server->team_id) !== null; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, S3Storage $storage): bool + { + return $user->teams()->where('id', $storage->team_id)->exists(); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, S3Storage $storage): bool + { + return false; + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, S3Storage $storage): bool + { + return false; + } +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index b960dd8e3..2d9910add 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -9,9 +9,12 @@ use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use SocialiteProviders\Authentik\AuthentikExtendSocialite; use SocialiteProviders\Azure\AzureExtendSocialite; +use SocialiteProviders\Clerk\ClerkExtendSocialite; +use SocialiteProviders\Discord\DiscordExtendSocialite; use SocialiteProviders\Google\GoogleExtendSocialite; use SocialiteProviders\Infomaniak\InfomaniakExtendSocialite; use SocialiteProviders\Manager\SocialiteWasCalled; +use SocialiteProviders\Zitadel\ZitadelExtendSocialite; class EventServiceProvider extends ServiceProvider { @@ -25,8 +28,11 @@ class EventServiceProvider extends ServiceProvider SocialiteWasCalled::class => [ AzureExtendSocialite::class.'@handle', AuthentikExtendSocialite::class.'@handle', + ClerkExtendSocialite::class.'@handle', + DiscordExtendSocialite::class.'@handle', GoogleExtendSocialite::class.'@handle', InfomaniakExtendSocialite::class.'@handle', + ZitadelExtendSocialite::class.'@handle', ], ]; diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index c85960746..2150126cd 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -49,7 +49,7 @@ protected function configureRateLimiting(): void return Limit::perMinute(1000)->by($request->user()?->id ?: $request->ip()); } - return Limit::perMinute(200)->by($request->user()?->id ?: $request->ip()); + return Limit::perMinute((int) config('api.rate_limit'))->by($request->user()?->id ?: $request->ip()); }); RateLimiter::for('5', function (Request $request) { return Limit::perMinute(5)->by($request->user()?->id ?: $request->ip()); diff --git a/app/View/Components/services/advanced.php b/app/View/Components/Services/Advanced.php similarity index 85% rename from app/View/Components/services/advanced.php rename to app/View/Components/Services/Advanced.php index 8104eaad4..99729a262 100644 --- a/app/View/Components/services/advanced.php +++ b/app/View/Components/Services/Advanced.php @@ -1,13 +1,13 @@ push('traefik.enable=true'); - $labels->push('traefik.http.middlewares.gzip.compress=true'); + if ($is_gzip_enabled) { + $labels->push('traefik.http.middlewares.gzip.compress=true'); + } $labels->push('traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https'); $is_http_basic_auth_enabled = $is_http_basic_auth_enabled && $http_basic_auth_username !== null && $http_basic_auth_password !== null; diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index cd99713a2..1e1d2a073 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -119,70 +119,62 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $resourceFqdns = str($resource->fqdn)->explode(','); if ($resourceFqdns->count() === 1) { $resourceFqdns = $resourceFqdns->first(); - $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', ''); - $generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class) - ->where('resourceable_id', $resource->service_id) - ->where('key', $variableName) - ->first(); + $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '_'); $fqdn = Url::fromString($resourceFqdns); $port = $fqdn->getPort(); $path = $fqdn->getPath(); $fqdn = $fqdn->getScheme().'://'.$fqdn->getHost(); - if ($generatedEnv) { - if ($path === '/') { - $generatedEnv->value = $fqdn; - } else { - $generatedEnv->value = $fqdn.$path; - } - $generatedEnv->save(); - } + $fqdnValue = ($path === '/') ? $fqdn : $fqdn.$path; + EnvironmentVariable::updateOrCreate([ + 'resourceable_type' => Service::class, + 'resourceable_id' => $resource->service_id, + 'key' => $variableName, + ], [ + 'value' => $fqdnValue, + 'is_build_time' => false, + 'is_preview' => false, + ]); if ($port) { $variableName = $variableName."_$port"; - $generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class) - ->where('resourceable_id', $resource->service_id) - ->where('key', $variableName) - ->first(); - if ($generatedEnv) { - if ($path === '/') { - $generatedEnv->value = $fqdn; - } else { - $generatedEnv->value = $fqdn.$path; - } - $generatedEnv->save(); - } + EnvironmentVariable::updateOrCreate([ + 'resourceable_type' => Service::class, + 'resourceable_id' => $resource->service_id, + 'key' => $variableName, + ], [ + 'value' => $fqdnValue, + 'is_build_time' => false, + 'is_preview' => false, + ]); } - $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', ''); - $generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class) - ->where('resourceable_id', $resource->service_id) - ->where('key', $variableName) - ->first(); + $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_'); $url = Url::fromString($fqdn); $port = $url->getPort(); $path = $url->getPath(); $url = $url->getHost(); - if ($generatedEnv) { - $url = str($fqdn)->after('://'); - if ($path === '/') { - $generatedEnv->value = $url; - } else { - $generatedEnv->value = $url.$path; - } - $generatedEnv->save(); + $urlValue = str($fqdn)->after('://'); + if ($path !== '/') { + $urlValue = $urlValue.$path; } + EnvironmentVariable::updateOrCreate([ + 'resourceable_type' => Service::class, + 'resourceable_id' => $resource->service_id, + 'key' => $variableName, + ], [ + 'value' => $urlValue, + 'is_build_time' => false, + 'is_preview' => false, + ]); if ($port) { $variableName = $variableName."_$port"; - $generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class) - ->where('resourceable_id', $resource->service_id) - ->where('key', $variableName) - ->first(); - if ($generatedEnv) { - if ($path === '/') { - $generatedEnv->value = $url; - } else { - $generatedEnv->value = $url.$path; - } - $generatedEnv->save(); - } + EnvironmentVariable::updateOrCreate([ + 'resourceable_type' => Service::class, + 'resourceable_id' => $resource->service_id, + 'key' => $variableName, + ], [ + 'value' => $urlValue, + 'is_build_time' => false, + 'is_preview' => false, + ]); } } elseif ($resourceFqdns->count() > 1) { foreach ($resourceFqdns as $fqdn) { @@ -243,7 +235,7 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $port_env_url->save(); } } else { - $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', ''); + $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '_'); $generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class) ->where('resourceable_id', $resource->service_id) ->where('key', $variableName) @@ -254,7 +246,7 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $generatedEnv->value = $fqdn; $generatedEnv->save(); } - $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', ''); + $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_'); $generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class) ->where('resourceable_id', $resource->service_id) ->where('key', $variableName) @@ -269,6 +261,17 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) } } } + } else { + // If FQDN is removed, delete the corresponding environment variables + $serviceName = str($resource->name)->upper()->replace('-', '_'); + EnvironmentVariable::where('resourceable_type', Service::class) + ->where('resourceable_id', $resource->service_id) + ->where('key', 'LIKE', "SERVICE_FQDN_{$serviceName}%") + ->delete(); + EnvironmentVariable::where('resourceable_type', Service::class) + ->where('resourceable_id', $resource->service_id) + ->where('key', 'LIKE', "SERVICE_URL_{$serviceName}%") + ->delete(); } } catch (\Throwable $e) { return handleError($e); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 2f1b934bb..7ce511f2c 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -599,7 +599,15 @@ function getTopLevelNetworks(Service|Application $resource) try { $yaml = Yaml::parse($resource->docker_compose_raw); } catch (\Exception $e) { - throw new \RuntimeException($e->getMessage()); + // If the docker-compose.yml file is not valid, we will return the network name as the key + $topLevelNetworks = collect([ + $resource->uuid => [ + 'name' => $resource->uuid, + 'external' => true, + ], + ]); + + return $topLevelNetworks->keys(); } $services = data_get($yaml, 'services'); $topLevelNetworks = collect(data_get($yaml, 'networks', [])); @@ -653,9 +661,16 @@ function getTopLevelNetworks(Service|Application $resource) try { $yaml = Yaml::parse($resource->docker_compose_raw); } catch (\Exception $e) { - throw new \RuntimeException($e->getMessage()); + // If the docker-compose.yml file is not valid, we will return the network name as the key + $topLevelNetworks = collect([ + $resource->uuid => [ + 'name' => $resource->uuid, + 'external' => true, + ], + ]); + + return $topLevelNetworks->keys(); } - $server = $resource->destination->server; $topLevelNetworks = collect(data_get($yaml, 'networks', [])); $services = data_get($yaml, 'services'); $definedNetwork = collect([$resource->uuid]); @@ -2931,7 +2946,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } catch (\Exception) { return collect([]); } - $services = data_get($yaml, 'services', collect([])); $topLevel = collect([ 'volumes' => collect(data_get($yaml, 'volumes', [])), @@ -2991,12 +3005,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first(); if ($applicationFound) { $savedService = $applicationFound; - // $savedService = ServiceDatabase::firstOrCreate([ - // 'name' => $applicationFound->name, - // 'image' => $applicationFound->image, - // 'service_id' => $applicationFound->service_id, - // ]); - // $applicationFound->delete(); } else { $savedService = ServiceDatabase::firstOrCreate([ 'name' => $serviceName, @@ -3007,15 +3015,22 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $savedService = ServiceApplication::firstOrCreate([ 'name' => $serviceName, 'service_id' => $resource->id, + ], [ + 'is_gzip_enabled' => true, ]); } - // Check if image changed if ($savedService->image !== $image) { $savedService->image = $image; $savedService->save(); } + // Pocketbase does not need gzip for SSE. + if (str($savedService->image)->contains('pocketbase') && $savedService->is_gzip_enabled) { + $savedService->is_gzip_enabled = false; + $savedService->save(); + } } + $environment = collect(data_get($service, 'environment', [])); $buildArgs = collect(data_get($service, 'build.args', [])); $environment = $environment->merge($buildArgs); @@ -3034,7 +3049,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int // Get all SERVICE_ variables from keys and values $key = str($key); $value = str($value); - $regex = '/\$(\{?([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\}?)/'; preg_match_all($regex, $value, $valueMatches); if (count($valueMatches[1]) > 0) { @@ -3048,7 +3062,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } } } - // Get magic environments where we need to preset the FQDN if ($key->startsWith('SERVICE_FQDN_')) { // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000 @@ -3060,12 +3073,19 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $port = null; } if ($isApplication) { - $fqdn = generateFqdn($server, "$uuid"); + $fqdn = $resource->fqdn; + if (blank($resource->fqdn)) { + $fqdn = generateFqdn($server, "$uuid"); + } } elseif ($isService) { - if ($fqdnFor) { - $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); + if (blank($savedService->fqdn)) { + if ($fqdnFor) { + $fqdn = generateFqdn($server, "$fqdnFor-$uuid"); + } else { + $fqdn = generateFqdn($server, "{$savedService->name}-$uuid"); + } } else { - $fqdn = generateFqdn($server, "{$savedService->name}-$uuid"); + $fqdn = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value(); } } @@ -3090,7 +3110,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } if (substr_count(str($key)->value(), '_') === 2) { - $resource->environment_variables()->firstOrCreate([ + $resource->environment_variables()->updateOrCreate([ 'key' => $key->value(), 'resourceable_type' => get_class($resource), 'resourceable_id' => $resource->id, @@ -3102,7 +3122,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } if (substr_count(str($key)->value(), '_') === 3) { $newKey = str($key)->beforeLast('_'); - $resource->environment_variables()->firstOrCreate([ + $resource->environment_variables()->updateOrCreate([ 'key' => $newKey->value(), 'resourceable_type' => get_class($resource), 'resourceable_id' => $resource->id, @@ -3126,6 +3146,9 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int continue; } if ($command->value() === 'FQDN') { + if ($isApplication && $resource->build_pack === 'dockercompose') { + continue; + } $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); if (str($fqdnFor)->contains('_')) { $fqdnFor = str($fqdnFor)->before('_'); @@ -3141,6 +3164,13 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int 'is_preview' => false, ]); } elseif ($command->value() === 'URL') { + if ($isApplication && $resource->build_pack === 'dockercompose') { + continue; + } + // For services, only generate URL if explicit FQDN is set + if ($isService && blank($savedService->fqdn)) { + continue; + } $fqdnFor = $key->after('SERVICE_URL_')->lower()->value(); if (str($fqdnFor)->contains('_')) { $fqdnFor = str($fqdnFor)->before('_'); @@ -3591,7 +3621,8 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int 'is_required' => $isRequired, ]); // Add the variable to the environment so it will be shown in the deployable compose file - $environment[$parsedKeyValue->value()] = $resource->environment_variables()->where('key', $parsedKeyValue)->where('resourceable_type', get_class($resource))->where('resourceable_id', $resource->id)->first()->value; + // $environment[$parsedKeyValue->value()] = $resource->environment_variables()->where('key', $parsedKeyValue)->where('resourceable_type', get_class($resource))->where('resourceable_id', $resource->id)->first()->real_value; + $environment[$parsedKeyValue->value()] = $value; continue; } @@ -3629,9 +3660,30 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } if ($isApplication) { - $domains = collect(json_decode($resource->docker_compose_domains)) ?? collect([]); + if ($isPullRequest) { + $preview = $resource->previews()->find($preview_id); + $domains = collect(json_decode(data_get($preview, 'docker_compose_domains'))) ?? collect([]); + } else { + $domains = collect(json_decode($resource->docker_compose_domains)) ?? collect([]); + } $fqdns = data_get($domains, "$serviceName.domain"); - if ($fqdns) { + // Generate SERVICE_FQDN & SERVICE_URL for dockercompose + if ($resource->build_pack === 'dockercompose') { + foreach ($domains as $forServiceName => $domain) { + $parsedDomain = data_get($domain, 'domain'); + if (filled($parsedDomain)) { + $parsedDomain = str($parsedDomain)->explode(',')->first(); + $coolifyUrl = Url::fromString($parsedDomain); + $coolifyScheme = $coolifyUrl->getScheme(); + $coolifyFqdn = $coolifyUrl->getHost(); + $coolifyUrl = $coolifyUrl->withScheme($coolifyScheme)->withHost($coolifyFqdn)->withPort(null); + $coolifyEnvironments->put('SERVICE_URL_'.str($forServiceName)->upper()->replace('-', '_'), $coolifyUrl->__toString()); + $coolifyEnvironments->put('SERVICE_FQDN_'.str($forServiceName)->upper()->replace('-', '_'), $coolifyFqdn); + } + } + } + // If the domain is set, we need to generate the FQDNs for the preview + if (filled($fqdns)) { $fqdns = str($fqdns)->explode(','); if ($isPullRequest) { $preview = $resource->previews()->find($preview_id); @@ -3663,7 +3715,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } } } - $defaultLabels = defaultLabels( id: $resource->id, name: $containerName, @@ -3673,6 +3724,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int type: 'application', environment: $resource->environment->name, ); + } elseif ($isService) { if ($savedService->serviceType()) { $fqdns = generateServiceSpecificFqdns($savedService); @@ -3694,10 +3746,13 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } // Add COOLIFY_FQDN & COOLIFY_URL to environment if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { - $coolifyEnvironments->put('COOLIFY_URL', $fqdns->implode(',')); + $fqdnsWithoutPort = $fqdns->map(function ($fqdn) { + return str($fqdn)->after('://')->before(':')->prepend(str($fqdn)->before('://')->append('://')); + }); + $coolifyEnvironments->put('COOLIFY_URL', $fqdnsWithoutPort->implode(',')); $urls = $fqdns->map(function ($fqdn) { - return str($fqdn)->replace('http://', '')->replace('https://', ''); + return str($fqdn)->replace('http://', '')->replace('https://', '')->before(':'); }); $coolifyEnvironments->put('COOLIFY_FQDN', $urls->implode(',')); } diff --git a/bootstrap/helpers/socialite.php b/bootstrap/helpers/socialite.php index fe19752cb..961f6809b 100644 --- a/bootstrap/helpers/socialite.php +++ b/bootstrap/helpers/socialite.php @@ -22,15 +22,26 @@ function get_socialite_provider(string $provider) return Socialite::driver('azure')->setConfig($azure_config); } - if ($provider == 'authentik') { - $authentik_config = new \SocialiteProviders\Manager\Config( + if ($provider == 'authentik' || $provider == 'clerk') { + $authentik_clerk_config = new \SocialiteProviders\Manager\Config( $oauth_setting->client_id, $oauth_setting->client_secret, $oauth_setting->redirect_uri, ['base_url' => $oauth_setting->base_url], ); - return Socialite::driver('authentik')->setConfig($authentik_config); + return Socialite::driver($provider)->setConfig($authentik_clerk_config); + } + + if ($provider == 'zitadel') { + $zitadel_config = new \SocialiteProviders\Manager\Config( + $oauth_setting->client_id, + $oauth_setting->client_secret, + $oauth_setting->redirect_uri, + ['base_url' => $oauth_setting->base_url], + ); + + return Socialite::driver('zitadel')->setConfig($zitadel_config); } if ($provider == 'google') { @@ -53,6 +64,7 @@ function get_socialite_provider(string $provider) $provider_class_map = [ 'bitbucket' => \Laravel\Socialite\Two\BitbucketProvider::class, + 'discord' => \SocialiteProviders\Discord\Provider::class, 'github' => \Laravel\Socialite\Two\GithubProvider::class, 'gitlab' => \Laravel\Socialite\Two\GitlabProvider::class, 'infomaniak' => \SocialiteProviders\Infomaniak\Provider::class, diff --git a/composer.json b/composer.json index 3fb1206ce..68b0fb066 100644 --- a/composer.json +++ b/composer.json @@ -13,62 +13,65 @@ "require": { "php": "^8.4", "danharrin/livewire-rate-limiting": "^2.1.0", - "doctrine/dbal": "^4.2.2", - "guzzlehttp/guzzle": "^7.9.2", - "laravel/fortify": "^1.25.4", - "laravel/framework": "^12.4.1", - "laravel/horizon": "^5.30.3", - "laravel/pail": "^1.2.2", - "laravel/prompts": "^0.3.5|^0.3.5|^0.3.5", - "laravel/sanctum": "^4.0.8", - "laravel/socialite": "^5.18.0", + "doctrine/dbal": "^4.3.0", + "guzzlehttp/guzzle": "^7.9.3", + "laravel/fortify": "^1.27.0", + "laravel/framework": "^12.20.0", + "laravel/horizon": "^5.33.1", + "laravel/pail": "^1.2.3", + "laravel/prompts": "^0.3.6|^0.3.6|^0.3.6", + "laravel/sanctum": "^4.1.2", + "laravel/socialite": "^5.21.0", "laravel/tinker": "^2.10.1", "laravel/ui": "^4.6.1", "lcobucci/jwt": "^5.5.0", "league/flysystem-aws-s3-v3": "^3.29", - "league/flysystem-sftp-v3": "^3.29", - "livewire/livewire": "^3.5.20", + "league/flysystem-sftp-v3": "^3.30", + "livewire/livewire": "^3.6.4", "log1x/laravel-webfonts": "^2.0.1", - "lorisleiva/laravel-actions": "^2.8.6", + "lorisleiva/laravel-actions": "^2.9.0", "nubs/random-name-generator": "^2.2", - "phpseclib/phpseclib": "^3.0.43", - "pion/laravel-chunk-upload": "^1.5.4", + "phpseclib/phpseclib": "^3.0.46", + "pion/laravel-chunk-upload": "^1.5.6", "poliander/cron": "^3.2.1", "purplepixie/phpdns": "^2.2", "pusher/pusher-php-server": "^7.2.7", - "resend/resend-laravel": "^0.17.0", - "sentry/sentry-laravel": "^4.13", + "resend/resend-laravel": "^0.19.0", + "sentry/sentry-laravel": "^4.15.1", "socialiteproviders/authentik": "^5.2", + "socialiteproviders/clerk": "^5.0", + "socialiteproviders/discord": "^4.2", "socialiteproviders/google": "^4.1", "socialiteproviders/infomaniak": "^4.0", "socialiteproviders/microsoft-azure": "^5.2", - "spatie/laravel-activitylog": "^4.10.1", - "spatie/laravel-data": "^4.13.1", - "spatie/laravel-ray": "^1.39.1", + "socialiteproviders/zitadel": "^4.2", + "spatie/laravel-activitylog": "^4.10.2", + "spatie/laravel-data": "^4.17.0", + "spatie/laravel-ray": "^1.40.2", "spatie/laravel-schemaless-attributes": "^2.5.1", "spatie/url": "^2.4", - "stevebauman/purify": "^6.3", - "stripe/stripe-php": "^16.5.1", - "symfony/yaml": "^7.2.3", + "stevebauman/purify": "^6.3.1", + "stripe/stripe-php": "^16.6.0", + "symfony/yaml": "^7.3.1", "visus/cuid2": "^4.1.0", "yosymfony/toml": "^1.0.4", - "zircote/swagger-php": "^5.0.5" + "zircote/swagger-php": "^5.1.4" }, "require-dev": { - "barryvdh/laravel-debugbar": "^3.15.1", - "driftingly/rector-laravel": "^2.0.2", + "barryvdh/laravel-debugbar": "^3.15.4", + "driftingly/rector-laravel": "^2.0.5", "fakerphp/faker": "^1.24.1", - "laravel/dusk": "^8.3.1", - "laravel/pint": "^1.21", - "laravel/telescope": "^5.5", + "laravel/dusk": "^8.3.3", + "laravel/pint": "^1.24", + "laravel/telescope": "^5.10", "mockery/mockery": "^1.6.12", - "nunomaduro/collision": "^8.6.1", - "pestphp/pest": "^3.8.0", - "phpstan/phpstan": "^2.1.6", - "rector/rector": "^2.0.9", + "nunomaduro/collision": "^8.8.2", + "pestphp/pest": "^3.8.2", + "phpstan/phpstan": "^2.1.18", + "rector/rector": "^2.1.2", "serversideup/spin": "^3.0.2", "spatie/laravel-ignition": "^2.9.1", - "symfony/http-client": "^7.2.3" + "symfony/http-client": "^7.3.1" }, "minimum-stability": "stable", "prefer-stable": true, diff --git a/composer.lock b/composer.lock index 14ae57fcb..acf153038 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ae74a752b941c9b981288c2934479eb8", + "content-hash": "52a680a0eb446dcaa74bc35e158aca57", "packages": [ { "name": "amphp/amp", @@ -870,16 +870,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.343.20", + "version": "3.352.0", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "bf40b00d2e1cbd2a5d8c903743073440d8ebb5dc" + "reference": "7f3ad0da2545b24259273ea7ab892188bae7d91b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/bf40b00d2e1cbd2a5d8c903743073440d8ebb5dc", - "reference": "bf40b00d2e1cbd2a5d8c903743073440d8ebb5dc", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7f3ad0da2545b24259273ea7ab892188bae7d91b", + "reference": "7f3ad0da2545b24259273ea7ab892188bae7d91b", "shasum": "" }, "require": { @@ -961,9 +961,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.343.20" + "source": "https://github.com/aws/aws-sdk-php/tree/3.352.0" }, - "time": "2025-05-28T18:10:03+00:00" + "time": "2025-08-01T18:04:23+00:00" }, { "name": "bacon/bacon-qr-code", @@ -1021,16 +1021,16 @@ }, { "name": "brick/math", - "version": "0.12.3", + "version": "0.13.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba" + "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba", - "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba", + "url": "https://api.github.com/repos/brick/math/zipball/fc7ed316430118cc7836bf45faff18d5dfc8de04", + "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04", "shasum": "" }, "require": { @@ -1069,7 +1069,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.12.3" + "source": "https://github.com/brick/math/tree/0.13.1" }, "funding": [ { @@ -1077,7 +1077,7 @@ "type": "github" } ], - "time": "2025-02-28T13:11:00+00:00" + "time": "2025-03-29T13:50:30+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -1373,34 +1373,34 @@ }, { "name": "doctrine/dbal", - "version": "4.2.3", + "version": "4.3.1", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "33d2d7fe1269b2301640c44cf2896ea607b30e3e" + "reference": "ac336c95ea9e13433d56ca81c308b39db0e1a2a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/33d2d7fe1269b2301640c44cf2896ea607b30e3e", - "reference": "33d2d7fe1269b2301640c44cf2896ea607b30e3e", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/ac336c95ea9e13433d56ca81c308b39db0e1a2a7", + "reference": "ac336c95ea9e13433d56ca81c308b39db0e1a2a7", "shasum": "" }, "require": { - "doctrine/deprecations": "^0.5.3|^1", - "php": "^8.1", + "doctrine/deprecations": "^1.1.5", + "php": "^8.2", "psr/cache": "^1|^2|^3", "psr/log": "^1|^2|^3" }, "require-dev": { - "doctrine/coding-standard": "12.0.0", + "doctrine/coding-standard": "13.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.2", - "phpstan/phpstan": "2.1.1", - "phpstan/phpstan-phpunit": "2.0.3", + "phpstan/phpstan": "2.1.17", + "phpstan/phpstan-phpunit": "2.0.6", "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "10.5.39", - "slevomat/coding-standard": "8.13.1", - "squizlabs/php_codesniffer": "3.10.2", + "phpunit/phpunit": "11.5.23", + "slevomat/coding-standard": "8.16.2", + "squizlabs/php_codesniffer": "3.13.1", "symfony/cache": "^6.3.8|^7.0", "symfony/console": "^5.4|^6.3|^7.0" }, @@ -1459,7 +1459,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/4.2.3" + "source": "https://github.com/doctrine/dbal/tree/4.3.1" }, "funding": [ { @@ -1475,7 +1475,7 @@ "type": "tidelift" } ], - "time": "2025-03-07T18:29:05+00:00" + "time": "2025-07-22T10:09:51+00:00" }, { "name": "doctrine/deprecations", @@ -2613,16 +2613,16 @@ }, { "name": "laravel/fortify", - "version": "v1.25.4", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/laravel/fortify.git", - "reference": "f185600e2d3a861834ad00ee3b7863f26ac25d3f" + "reference": "0fb2ec99dfee77ed66884668fc06683acca91ebd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/fortify/zipball/f185600e2d3a861834ad00ee3b7863f26ac25d3f", - "reference": "f185600e2d3a861834ad00ee3b7863f26ac25d3f", + "url": "https://api.github.com/repos/laravel/fortify/zipball/0fb2ec99dfee77ed66884668fc06683acca91ebd", + "reference": "0fb2ec99dfee77ed66884668fc06683acca91ebd", "shasum": "" }, "require": { @@ -2674,24 +2674,24 @@ "issues": "https://github.com/laravel/fortify/issues", "source": "https://github.com/laravel/fortify" }, - "time": "2025-01-26T19:34:46+00:00" + "time": "2025-06-11T14:30:52+00:00" }, { "name": "laravel/framework", - "version": "v12.16.0", + "version": "v12.21.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "293bb1c70224faebfd3d4328e201c37115da055f" + "reference": "ac8c4e73bf1b5387b709f7736d41427e6af1c93b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/293bb1c70224faebfd3d4328e201c37115da055f", - "reference": "293bb1c70224faebfd3d4328e201c37115da055f", + "url": "https://api.github.com/repos/laravel/framework/zipball/ac8c4e73bf1b5387b709f7736d41427e6af1c93b", + "reference": "ac8c4e73bf1b5387b709f7736d41427e6af1c93b", "shasum": "" }, "require": { - "brick/math": "^0.11|^0.12", + "brick/math": "^0.11|^0.12|^0.13", "composer-runtime-api": "^2.2", "doctrine/inflector": "^2.0.5", "dragonmantank/cron-expression": "^3.4", @@ -2889,20 +2889,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-05-27T15:49:44+00:00" + "time": "2025-07-22T15:41:55+00:00" }, { "name": "laravel/horizon", - "version": "v5.32.1", + "version": "v5.33.1", "source": { "type": "git", "url": "https://github.com/laravel/horizon.git", - "reference": "e78d9689d85b3d4769dc64def5eb6d94e5776beb" + "reference": "50057bca1f1dcc9fbd5ff6d65143833babd784b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/horizon/zipball/e78d9689d85b3d4769dc64def5eb6d94e5776beb", - "reference": "e78d9689d85b3d4769dc64def5eb6d94e5776beb", + "url": "https://api.github.com/repos/laravel/horizon/zipball/50057bca1f1dcc9fbd5ff6d65143833babd784b3", + "reference": "50057bca1f1dcc9fbd5ff6d65143833babd784b3", "shasum": "" }, "require": { @@ -2967,22 +2967,22 @@ ], "support": { "issues": "https://github.com/laravel/horizon/issues", - "source": "https://github.com/laravel/horizon/tree/v5.32.1" + "source": "https://github.com/laravel/horizon/tree/v5.33.1" }, - "time": "2025-05-19T13:13:30+00:00" + "time": "2025-06-16T13:48:30+00:00" }, { "name": "laravel/pail", - "version": "v1.2.2", + "version": "v1.2.3", "source": { "type": "git", "url": "https://github.com/laravel/pail.git", - "reference": "f31f4980f52be17c4667f3eafe034e6826787db2" + "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pail/zipball/f31f4980f52be17c4667f3eafe034e6826787db2", - "reference": "f31f4980f52be17c4667f3eafe034e6826787db2", + "url": "https://api.github.com/repos/laravel/pail/zipball/8cc3d575c1f0e57eeb923f366a37528c50d2385a", + "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a", "shasum": "" }, "require": { @@ -3002,7 +3002,7 @@ "orchestra/testbench-core": "^8.13|^9.0|^10.0", "pestphp/pest": "^2.20|^3.0", "pestphp/pest-plugin-type-coverage": "^2.3|^3.0", - "phpstan/phpstan": "^1.10", + "phpstan/phpstan": "^1.12.27", "symfony/var-dumper": "^6.3|^7.0" }, "type": "library", @@ -3038,6 +3038,7 @@ "description": "Easily delve into your Laravel application's log files directly from the command line.", "homepage": "https://github.com/laravel/pail", "keywords": [ + "dev", "laravel", "logs", "php", @@ -3047,20 +3048,20 @@ "issues": "https://github.com/laravel/pail/issues", "source": "https://github.com/laravel/pail" }, - "time": "2025-01-28T15:15:15+00:00" + "time": "2025-06-05T13:55:57+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.5", + "version": "v0.3.6", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1" + "reference": "86a8b692e8661d0fb308cec64f3d176821323077" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/57b8f7efe40333cdb925700891c7d7465325d3b1", - "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1", + "url": "https://api.github.com/repos/laravel/prompts/zipball/86a8b692e8661d0fb308cec64f3d176821323077", + "reference": "86a8b692e8661d0fb308cec64f3d176821323077", "shasum": "" }, "require": { @@ -3104,22 +3105,22 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.5" + "source": "https://github.com/laravel/prompts/tree/v0.3.6" }, - "time": "2025-02-11T13:34:40+00:00" + "time": "2025-07-07T14:17:42+00:00" }, { "name": "laravel/sanctum", - "version": "v4.1.1", + "version": "v4.2.0", "source": { "type": "git", "url": "https://github.com/laravel/sanctum.git", - "reference": "a360a6a1fd2400ead4eb9b6a9c1bb272939194f5" + "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sanctum/zipball/a360a6a1fd2400ead4eb9b6a9c1bb272939194f5", - "reference": "a360a6a1fd2400ead4eb9b6a9c1bb272939194f5", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677", + "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677", "shasum": "" }, "require": { @@ -3170,7 +3171,7 @@ "issues": "https://github.com/laravel/sanctum/issues", "source": "https://github.com/laravel/sanctum" }, - "time": "2025-04-23T13:03:38+00:00" + "time": "2025-07-09T19:45:24+00:00" }, { "name": "laravel/serializable-closure", @@ -3235,16 +3236,16 @@ }, { "name": "laravel/socialite", - "version": "v5.21.0", + "version": "v5.23.0", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "d83639499ad14985c9a6a9713b70073300ce998d" + "reference": "e9e0fc83b9d8d71c8385a5da20e5b95ca6234cf5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/d83639499ad14985c9a6a9713b70073300ce998d", - "reference": "d83639499ad14985c9a6a9713b70073300ce998d", + "url": "https://api.github.com/repos/laravel/socialite/zipball/e9e0fc83b9d8d71c8385a5da20e5b95ca6234cf5", + "reference": "e9e0fc83b9d8d71c8385a5da20e5b95ca6234cf5", "shasum": "" }, "require": { @@ -3303,7 +3304,7 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2025-05-19T12:56:37+00:00" + "time": "2025-07-23T14:16:08+00:00" }, { "name": "laravel/tinker", @@ -3509,16 +3510,16 @@ }, { "name": "league/commonmark", - "version": "2.7.0", + "version": "2.7.1", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405" + "reference": "10732241927d3971d28e7ea7b5712721fa2296ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", - "reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca", + "reference": "10732241927d3971d28e7ea7b5712721fa2296ca", "shasum": "" }, "require": { @@ -3547,7 +3548,7 @@ "symfony/process": "^5.4 | ^6.0 | ^7.0", "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", "unleashedtech/php-coding-standard": "^3.1.1", - "vimeo/psalm": "^4.24.0 || ^5.0.0" + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" }, "suggest": { "symfony/yaml": "v2.3+ required if using the Front Matter extension" @@ -3612,7 +3613,7 @@ "type": "tidelift" } ], - "time": "2025-05-05T12:20:28+00:00" + "time": "2025-07-20T12:47:49+00:00" }, { "name": "league/config", @@ -3698,16 +3699,16 @@ }, { "name": "league/flysystem", - "version": "3.29.1", + "version": "3.30.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319" + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/edc1bb7c86fab0776c3287dbd19b5fa278347319", - "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2203e3151755d874bb2943649dae1eb8533ac93e", + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e", "shasum": "" }, "require": { @@ -3731,13 +3732,13 @@ "composer/semver": "^3.0", "ext-fileinfo": "*", "ext-ftp": "*", - "ext-mongodb": "^1.3", + "ext-mongodb": "^1.3|^2", "ext-zip": "*", "friendsofphp/php-cs-fixer": "^3.5", "google/cloud-storage": "^1.23", "guzzlehttp/psr7": "^2.6", "microsoft/azure-storage-blob": "^1.1", - "mongodb/mongodb": "^1.2", + "mongodb/mongodb": "^1.2|^2", "phpseclib/phpseclib": "^3.0.36", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.5.11|^10.0", @@ -3775,9 +3776,9 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.29.1" + "source": "https://github.com/thephpleague/flysystem/tree/3.30.0" }, - "time": "2024-10-08T08:58:34+00:00" + "time": "2025-06-25T13:29:59+00:00" }, { "name": "league/flysystem-aws-s3-v3", @@ -3836,16 +3837,16 @@ }, { "name": "league/flysystem-local", - "version": "3.29.0", + "version": "3.30.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27" + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/e0e8d52ce4b2ed154148453d321e97c8e931bd27", - "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/6691915f77c7fb69adfb87dcd550052dc184ee10", + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10", "shasum": "" }, "require": { @@ -3879,22 +3880,22 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.29.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.0" }, - "time": "2024-08-09T21:24:39+00:00" + "time": "2025-05-21T10:34:19+00:00" }, { "name": "league/flysystem-sftp-v3", - "version": "3.29.0", + "version": "3.30.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-sftp-v3.git", - "reference": "ce9b209e2fbe33122c755ffc18eb4d5bd256f252" + "reference": "93f297837b5052f4cfee601b2e0352addd956448" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-sftp-v3/zipball/ce9b209e2fbe33122c755ffc18eb4d5bd256f252", - "reference": "ce9b209e2fbe33122c755ffc18eb4d5bd256f252", + "url": "https://api.github.com/repos/thephpleague/flysystem-sftp-v3/zipball/93f297837b5052f4cfee601b2e0352addd956448", + "reference": "93f297837b5052f4cfee601b2e0352addd956448", "shasum": "" }, "require": { @@ -3928,9 +3929,9 @@ "sftp" ], "support": { - "source": "https://github.com/thephpleague/flysystem-sftp-v3/tree/3.29.0" + "source": "https://github.com/thephpleague/flysystem-sftp-v3/tree/3.30.0" }, - "time": "2024-08-14T19:35:54+00:00" + "time": "2025-04-17T15:49:35+00:00" }, { "name": "league/mime-type-detection", @@ -4240,16 +4241,16 @@ }, { "name": "livewire/livewire", - "version": "v3.6.3", + "version": "v3.6.4", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "56aa1bb63a46e06181c56fa64717a7287e19115e" + "reference": "ef04be759da41b14d2d129e670533180a44987dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/56aa1bb63a46e06181c56fa64717a7287e19115e", - "reference": "56aa1bb63a46e06181c56fa64717a7287e19115e", + "url": "https://api.github.com/repos/livewire/livewire/zipball/ef04be759da41b14d2d129e670533180a44987dc", + "reference": "ef04be759da41b14d2d129e670533180a44987dc", "shasum": "" }, "require": { @@ -4304,7 +4305,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.6.3" + "source": "https://github.com/livewire/livewire/tree/v3.6.4" }, "funding": [ { @@ -4312,7 +4313,7 @@ "type": "github" } ], - "time": "2025-04-12T22:26:52+00:00" + "time": "2025-07-17T05:12:15+00:00" }, { "name": "log1x/laravel-webfonts", @@ -4695,16 +4696,16 @@ }, { "name": "nesbot/carbon", - "version": "3.9.1", + "version": "3.10.2", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "ced71f79398ece168e24f7f7710462f462310d4d" + "reference": "76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/ced71f79398ece168e24f7f7710462f462310d4d", - "reference": "ced71f79398ece168e24f7f7710462f462310d4d", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24", + "reference": "76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24", "shasum": "" }, "require": { @@ -4712,9 +4713,9 @@ "ext-json": "*", "php": "^8.1", "psr/clock": "^1.0", - "symfony/clock": "^6.3 || ^7.0", + "symfony/clock": "^6.3.12 || ^7.0", "symfony/polyfill-mbstring": "^1.0", - "symfony/translation": "^4.4.18 || ^5.2.1|| ^6.0 || ^7.0" + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -4722,14 +4723,13 @@ "require-dev": { "doctrine/dbal": "^3.6.3 || ^4.0", "doctrine/orm": "^2.15.2 || ^3.0", - "friendsofphp/php-cs-fixer": "^3.57.2", + "friendsofphp/php-cs-fixer": "^3.75.0", "kylekatarnls/multi-tester": "^2.5.3", - "ondrejmirtes/better-reflection": "^6.25.0.4", "phpmd/phpmd": "^2.15.0", - "phpstan/extension-installer": "^1.3.1", - "phpstan/phpstan": "^1.11.2", - "phpunit/phpunit": "^10.5.20", - "squizlabs/php_codesniffer": "^3.9.0" + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.17", + "phpunit/phpunit": "^10.5.46", + "squizlabs/php_codesniffer": "^3.13.0" }, "bin": [ "bin/carbon" @@ -4797,7 +4797,7 @@ "type": "tidelift" } ], - "time": "2025-05-01T19:51:51+00:00" + "time": "2025-08-02T09:36:06+00:00" }, { "name": "nette/schema", @@ -4863,16 +4863,16 @@ }, { "name": "nette/utils", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "ce708655043c7050eb050df361c5e313cf708309" + "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/ce708655043c7050eb050df361c5e313cf708309", - "reference": "ce708655043c7050eb050df361c5e313cf708309", + "url": "https://api.github.com/repos/nette/utils/zipball/e67c4061eb40b9c113b218214e42cb5a0dda28f2", + "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2", "shasum": "" }, "require": { @@ -4943,22 +4943,22 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.6" + "source": "https://github.com/nette/utils/tree/v4.0.7" }, - "time": "2025-03-30T21:06:30+00:00" + "time": "2025-06-03T04:55:08+00:00" }, { "name": "nikic/php-parser", - "version": "v5.4.0", + "version": "v5.6.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/221b0d0fdf1369c71047ad1d18bb5880017bbc56", + "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56", "shasum": "" }, "require": { @@ -5001,9 +5001,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.0" }, - "time": "2024-12-30T11:07:19+00:00" + "time": "2025-07-27T20:03:57+00:00" }, { "name": "nubs/random-name-generator", @@ -5488,16 +5488,16 @@ }, { "name": "php-di/php-di", - "version": "7.0.10", + "version": "7.0.11", "source": { "type": "git", "url": "https://github.com/PHP-DI/PHP-DI.git", - "reference": "0d1ed64126577e9a095b3204dcaee58cf76432c2" + "reference": "32f111a6d214564520a57831d397263e8946c1d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/0d1ed64126577e9a095b3204dcaee58cf76432c2", - "reference": "0d1ed64126577e9a095b3204dcaee58cf76432c2", + "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/32f111a6d214564520a57831d397263e8946c1d2", + "reference": "32f111a6d214564520a57831d397263e8946c1d2", "shasum": "" }, "require": { @@ -5545,7 +5545,7 @@ ], "support": { "issues": "https://github.com/PHP-DI/PHP-DI/issues", - "source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.10" + "source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.11" }, "funding": [ { @@ -5557,23 +5557,24 @@ "type": "tidelift" } ], - "time": "2025-04-22T08:53:15+00:00" + "time": "2025-06-03T07:45:57+00:00" }, { "name": "phpdocumentor/reflection", - "version": "6.1.0", + "version": "6.3.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/Reflection.git", - "reference": "bb4dea805a645553d6d989b23dad9f8041f39502" + "reference": "d91b3270832785602adcc24ae2d0974ba99a8ff8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/Reflection/zipball/bb4dea805a645553d6d989b23dad9f8041f39502", - "reference": "bb4dea805a645553d6d989b23dad9f8041f39502", + "url": "https://api.github.com/repos/phpDocumentor/Reflection/zipball/d91b3270832785602adcc24ae2d0974ba99a8ff8", + "reference": "d91b3270832785602adcc24ae2d0974ba99a8ff8", "shasum": "" }, "require": { + "composer-runtime-api": "^2", "nikic/php-parser": "~4.18 || ^5.0", "php": "8.1.*|8.2.*|8.3.*|8.4.*", "phpdocumentor/reflection-common": "^2.1", @@ -5584,7 +5585,8 @@ }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "doctrine/coding-standard": "^12.0", + "doctrine/coding-standard": "^13.0", + "eliashaeussler/phpunit-attributes": "^1.7", "mikey179/vfsstream": "~1.2", "mockery/mockery": "~1.6.0", "phpspec/prophecy-phpunit": "^2.0", @@ -5592,7 +5594,7 @@ "phpstan/phpstan": "^1.8", "phpstan/phpstan-webmozart-assert": "^1.2", "phpunit/phpunit": "^10.0", - "psalm/phar": "^5.24", + "psalm/phar": "^6.0", "rector/rector": "^1.0.0", "squizlabs/php_codesniffer": "^3.8" }, @@ -5604,6 +5606,9 @@ } }, "autoload": { + "files": [ + "src/php-parser/Modifiers.php" + ], "psr-4": { "phpDocumentor\\": "src/phpDocumentor" } @@ -5622,9 +5627,9 @@ ], "support": { "issues": "https://github.com/phpDocumentor/Reflection/issues", - "source": "https://github.com/phpDocumentor/Reflection/tree/6.1.0" + "source": "https://github.com/phpDocumentor/Reflection/tree/6.3.0" }, - "time": "2024-11-22T15:11:54+00:00" + "time": "2025-06-06T13:39:18+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -5878,16 +5883,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.43", + "version": "3.0.46", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "709ec107af3cb2f385b9617be72af8cf62441d02" + "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/709ec107af3cb2f385b9617be72af8cf62441d02", - "reference": "709ec107af3cb2f385b9617be72af8cf62441d02", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", + "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", "shasum": "" }, "require": { @@ -5968,7 +5973,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.43" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.46" }, "funding": [ { @@ -5984,20 +5989,20 @@ "type": "tidelift" } ], - "time": "2024-12-14T21:12:59+00:00" + "time": "2025-06-26T16:29:55+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "2.1.0", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" + "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", - "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/b9e61a61e39e02dd90944e9115241c7f7e76bfd8", + "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8", "shasum": "" }, "require": { @@ -6029,9 +6034,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.2.0" }, - "time": "2025-02-19T13:28:12+00:00" + "time": "2025-07-13T07:04:09+00:00" }, { "name": "pion/laravel-chunk-upload", @@ -6658,16 +6663,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.8", + "version": "v0.12.10", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "85057ceedee50c49d4f6ecaff73ee96adb3b3625" + "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/85057ceedee50c49d4f6ecaff73ee96adb3b3625", - "reference": "85057ceedee50c49d4f6ecaff73ee96adb3b3625", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/6e80abe6f2257121f1eb9a4c55bf29d921025b22", + "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22", "shasum": "" }, "require": { @@ -6717,12 +6722,11 @@ "authors": [ { "name": "Justin Hileman", - "email": "justin@justinhileman.info", - "homepage": "http://justinhileman.com" + "email": "justin@justinhileman.info" } ], "description": "An interactive shell for modern PHP.", - "homepage": "http://psysh.org", + "homepage": "https://psysh.org", "keywords": [ "REPL", "console", @@ -6731,9 +6735,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.8" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.10" }, - "time": "2025-03-16T03:05:19+00:00" + "time": "2025-08-04T12:39:37+00:00" }, { "name": "purplepixie/phpdns", @@ -6966,21 +6970,20 @@ }, { "name": "ramsey/uuid", - "version": "4.7.6", + "version": "4.9.0", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "91039bc1faa45ba123c4328958e620d382ec7088" + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", - "reference": "91039bc1faa45ba123c4328958e620d382ec7088", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/4e0e23cc785f0724a0e838279a9eb03f28b092a0", + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0", "shasum": "" }, "require": { - "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12", - "ext-json": "*", + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -6988,26 +6991,23 @@ "rhumsaa/uuid": "self.version" }, "require-dev": { - "captainhook/captainhook": "^5.10", + "captainhook/captainhook": "^5.25", "captainhook/plugin-composer": "^5.3", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "doctrine/annotations": "^1.8", - "ergebnis/composer-normalize": "^2.15", - "mockery/mockery": "^1.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", "paragonie/random-lib": "^2", - "php-mock/php-mock": "^2.2", - "php-mock/php-mock-mockery": "^1.3", - "php-parallel-lint/php-parallel-lint": "^1.1", - "phpbench/phpbench": "^1.0", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-phpunit": "^1.1", - "phpunit/phpunit": "^8.5 || ^9", - "ramsey/composer-repl": "^1.4", - "slevomat/coding-standard": "^8.4", - "squizlabs/php_codesniffer": "^3.5", - "vimeo/psalm": "^4.9" + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" }, "suggest": { "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", @@ -7042,39 +7042,29 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.7.6" + "source": "https://github.com/ramsey/uuid/tree/4.9.0" }, - "funding": [ - { - "url": "https://github.com/ramsey", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", - "type": "tidelift" - } - ], - "time": "2024-04-27T21:32:50+00:00" + "time": "2025-06-25T14:20:11+00:00" }, { "name": "resend/resend-laravel", - "version": "v0.17.0", + "version": "v0.19.0", "source": { "type": "git", "url": "https://github.com/resend/resend-laravel.git", - "reference": "fbdf51872ff296af545d72acc6de03c0d910202c" + "reference": "ce11e363c42c1d6b93983dfebbaba3f906863c3a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/resend/resend-laravel/zipball/fbdf51872ff296af545d72acc6de03c0d910202c", - "reference": "fbdf51872ff296af545d72acc6de03c0d910202c", + "url": "https://api.github.com/repos/resend/resend-laravel/zipball/ce11e363c42c1d6b93983dfebbaba3f906863c3a", + "reference": "ce11e363c42c1d6b93983dfebbaba3f906863c3a", "shasum": "" }, "require": { "illuminate/http": "^10.0|^11.0|^12.0", "illuminate/support": "^10.0|^11.0|^12.0", "php": "^8.1", - "resend/resend-php": "^0.16.0", + "resend/resend-php": "^0.18.0", "symfony/mailer": "^6.2|^7.0" }, "require-dev": { @@ -7121,22 +7111,22 @@ ], "support": { "issues": "https://github.com/resend/resend-laravel/issues", - "source": "https://github.com/resend/resend-laravel/tree/v0.17.0" + "source": "https://github.com/resend/resend-laravel/tree/v0.19.0" }, - "time": "2025-03-25T00:42:52+00:00" + "time": "2025-05-06T21:36:51+00:00" }, { "name": "resend/resend-php", - "version": "v0.16.0", + "version": "v0.18.1", "source": { "type": "git", "url": "https://github.com/resend/resend-php.git", - "reference": "6e9be898c9e0035a5da3c2904e86d0b12999c2fc" + "reference": "f20a9a50a7f705294af1662995c93bfd806fed3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/resend/resend-php/zipball/6e9be898c9e0035a5da3c2904e86d0b12999c2fc", - "reference": "6e9be898c9e0035a5da3c2904e86d0b12999c2fc", + "url": "https://api.github.com/repos/resend/resend-php/zipball/f20a9a50a7f705294af1662995c93bfd806fed3e", + "reference": "f20a9a50a7f705294af1662995c93bfd806fed3e", "shasum": "" }, "require": { @@ -7178,9 +7168,9 @@ ], "support": { "issues": "https://github.com/resend/resend-php/issues", - "source": "https://github.com/resend/resend-php/tree/v0.16.0" + "source": "https://github.com/resend/resend-php/tree/v0.18.1" }, - "time": "2025-03-24T22:12:48+00:00" + "time": "2025-07-04T00:12:53+00:00" }, { "name": "revolt/event-loop", @@ -7256,16 +7246,16 @@ }, { "name": "sentry/sentry", - "version": "4.11.1", + "version": "4.14.2", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-php.git", - "reference": "53dc0bcb6a667cac5b760b46f98d5380e63e02ca" + "reference": "bfeec74303d60d3f8bc33701ab3e86f8a8729f17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/53dc0bcb6a667cac5b760b46f98d5380e63e02ca", - "reference": "53dc0bcb6a667cac5b760b46f98d5380e63e02ca", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/bfeec74303d60d3f8bc33701ab3e86f8a8729f17", + "reference": "bfeec74303d60d3f8bc33701ab3e86f8a8729f17", "shasum": "" }, "require": { @@ -7329,7 +7319,7 @@ ], "support": { "issues": "https://github.com/getsentry/sentry-php/issues", - "source": "https://github.com/getsentry/sentry-php/tree/4.11.1" + "source": "https://github.com/getsentry/sentry-php/tree/4.14.2" }, "funding": [ { @@ -7341,27 +7331,27 @@ "type": "custom" } ], - "time": "2025-05-12T11:30:33+00:00" + "time": "2025-07-21T08:28:29+00:00" }, { "name": "sentry/sentry-laravel", - "version": "4.13.0", + "version": "4.15.1", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-laravel.git", - "reference": "d232ac494258e0d50a77c575a5af5f1a426d3f87" + "reference": "7e0675e8e06d1ec5cb623792892920000a3aedb5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/d232ac494258e0d50a77c575a5af5f1a426d3f87", - "reference": "d232ac494258e0d50a77c575a5af5f1a426d3f87", + "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/7e0675e8e06d1ec5cb623792892920000a3aedb5", + "reference": "7e0675e8e06d1ec5cb623792892920000a3aedb5", "shasum": "" }, "require": { "illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0", "nyholm/psr7": "^1.0", "php": "^7.2 | ^8.0", - "sentry/sentry": "^4.10", + "sentry/sentry": "^4.14.1", "symfony/psr-http-message-bridge": "^1.0 | ^2.0 | ^6.0 | ^7.0" }, "require-dev": { @@ -7418,7 +7408,7 @@ ], "support": { "issues": "https://github.com/getsentry/sentry-laravel/issues", - "source": "https://github.com/getsentry/sentry-laravel/tree/4.13.0" + "source": "https://github.com/getsentry/sentry-laravel/tree/4.15.1" }, "funding": [ { @@ -7430,7 +7420,7 @@ "type": "custom" } ], - "time": "2025-02-18T10:09:29+00:00" + "time": "2025-06-24T12:39:03+00:00" }, { "name": "socialiteproviders/authentik", @@ -7482,6 +7472,106 @@ }, "time": "2023-11-07T22:21:16+00:00" }, + { + "name": "socialiteproviders/clerk", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/Clerk.git", + "reference": "41e123036001ff37851b9622a910010c0e487d6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/Clerk/zipball/41e123036001ff37851b9622a910010c0e487d6a", + "reference": "41e123036001ff37851b9622a910010c0e487d6a", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^8.0", + "socialiteproviders/manager": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "SocialiteProviders\\Clerk\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignacio Cano", + "email": "dev@nacho.sh" + } + ], + "description": "Clerk OAuth2 Provider for Laravel Socialite", + "keywords": [ + "clerk", + "laravel", + "oauth", + "provider", + "socialite" + ], + "support": { + "docs": "https://socialiteproviders.com/clerk", + "issues": "https://github.com/socialiteproviders/providers/issues", + "source": "https://github.com/socialiteproviders/providers" + }, + "time": "2024-02-19T12:17:59+00:00" + }, + { + "name": "socialiteproviders/discord", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/Discord.git", + "reference": "c71c379acfdca5ba4aa65a3db5ae5222852a919c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/Discord/zipball/c71c379acfdca5ba4aa65a3db5ae5222852a919c", + "reference": "c71c379acfdca5ba4aa65a3db5ae5222852a919c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.4 || ^8.0", + "socialiteproviders/manager": "~4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "SocialiteProviders\\Discord\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christopher Eklund", + "email": "eklundchristopher@gmail.com" + } + ], + "description": "Discord OAuth2 Provider for Laravel Socialite", + "keywords": [ + "discord", + "laravel", + "oauth", + "provider", + "socialite" + ], + "support": { + "docs": "https://socialiteproviders.com/discord", + "issues": "https://github.com/socialiteproviders/providers/issues", + "source": "https://github.com/socialiteproviders/providers" + }, + "time": "2023-07-24T23:28:47+00:00" + }, { "name": "socialiteproviders/google", "version": "4.1.0", @@ -7699,6 +7789,56 @@ }, "time": "2024-03-15T03:02:10+00:00" }, + { + "name": "socialiteproviders/zitadel", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/Zitadel.git", + "reference": "852ceebd71503ac116c42b8aa352e22b4fd396d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/Zitadel/zipball/852ceebd71503ac116c42b8aa352e22b4fd396d9", + "reference": "852ceebd71503ac116c42b8aa352e22b4fd396d9", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^8.0", + "socialiteproviders/manager": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "SocialiteProviders\\Zitadel\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gurkirat Singh", + "email": "tbhaxor@gmail.com" + } + ], + "description": "Zitadel OAuth2 Provider for Laravel Socialite", + "keywords": [ + "laravel", + "oauth", + "provider", + "socialite", + "zitadel" + ], + "support": { + "docs": "https://socialiteproviders.com/zitadel", + "issues": "https://github.com/socialiteproviders/providers/issues", + "source": "https://github.com/socialiteproviders/providers" + }, + "time": "2024-11-07T21:57:40+00:00" + }, { "name": "spatie/backtrace", "version": "1.7.4", @@ -7764,16 +7904,16 @@ }, { "name": "spatie/laravel-activitylog", - "version": "4.10.1", + "version": "4.10.2", "source": { "type": "git", "url": "https://github.com/spatie/laravel-activitylog.git", - "reference": "466f30f7245fe3a6e328ad5e6812bd43b4bddea5" + "reference": "bb879775d487438ed9a99e64f09086b608990c10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/466f30f7245fe3a6e328ad5e6812bd43b4bddea5", - "reference": "466f30f7245fe3a6e328ad5e6812bd43b4bddea5", + "url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/bb879775d487438ed9a99e64f09086b608990c10", + "reference": "bb879775d487438ed9a99e64f09086b608990c10", "shasum": "" }, "require": { @@ -7839,7 +7979,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-activitylog/issues", - "source": "https://github.com/spatie/laravel-activitylog/tree/4.10.1" + "source": "https://github.com/spatie/laravel-activitylog/tree/4.10.2" }, "funding": [ { @@ -7851,20 +7991,20 @@ "type": "github" } ], - "time": "2025-02-10T15:38:25+00:00" + "time": "2025-06-15T06:59:49+00:00" }, { "name": "spatie/laravel-data", - "version": "4.15.1", + "version": "4.17.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-data.git", - "reference": "cb97afe6c0dadeb2e76ea1b7220cd04ed33dcca9" + "reference": "6b110d25ad4219774241b083d09695b20a7fb472" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-data/zipball/cb97afe6c0dadeb2e76ea1b7220cd04ed33dcca9", - "reference": "cb97afe6c0dadeb2e76ea1b7220cd04ed33dcca9", + "url": "https://api.github.com/repos/spatie/laravel-data/zipball/6b110d25ad4219774241b083d09695b20a7fb472", + "reference": "6b110d25ad4219774241b083d09695b20a7fb472", "shasum": "" }, "require": { @@ -7926,7 +8066,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-data/issues", - "source": "https://github.com/spatie/laravel-data/tree/4.15.1" + "source": "https://github.com/spatie/laravel-data/tree/4.17.0" }, "funding": [ { @@ -7934,20 +8074,20 @@ "type": "github" } ], - "time": "2025-04-10T06:06:27+00:00" + "time": "2025-06-25T11:36:37+00:00" }, { "name": "spatie/laravel-package-tools", - "version": "1.92.4", + "version": "1.92.7", "source": { "type": "git", "url": "https://github.com/spatie/laravel-package-tools.git", - "reference": "d20b1969f836d210459b78683d85c9cd5c5f508c" + "reference": "f09a799850b1ed765103a4f0b4355006360c49a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/d20b1969f836d210459b78683d85c9cd5c5f508c", - "reference": "d20b1969f836d210459b78683d85c9cd5c5f508c", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/f09a799850b1ed765103a4f0b4355006360c49a5", + "reference": "f09a799850b1ed765103a4f0b4355006360c49a5", "shasum": "" }, "require": { @@ -7987,7 +8127,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-package-tools/issues", - "source": "https://github.com/spatie/laravel-package-tools/tree/1.92.4" + "source": "https://github.com/spatie/laravel-package-tools/tree/1.92.7" }, "funding": [ { @@ -7995,7 +8135,7 @@ "type": "github" } ], - "time": "2025-04-11T15:27:14+00:00" + "time": "2025-07-17T15:46:43+00:00" }, { "name": "spatie/laravel-ray", @@ -8638,16 +8778,16 @@ }, { "name": "symfony/console", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44" + "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/66c1440edf6f339fd82ed6c7caa76cb006211b44", - "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44", + "url": "https://api.github.com/repos/symfony/console/zipball/5f360ebc65c55265a74d23d7fe27f957870158a1", + "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1", "shasum": "" }, "require": { @@ -8712,7 +8852,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.0" + "source": "https://github.com/symfony/console/tree/v7.3.2" }, "funding": [ { @@ -8723,12 +8863,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-24T10:34:04+00:00" + "time": "2025-07-30T17:13:41+00:00" }, { "name": "symfony/css-selector", @@ -8864,16 +9008,16 @@ }, { "name": "symfony/error-handler", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "cf68d225bc43629de4ff54778029aee6dc191b83" + "reference": "0b31a944fcd8759ae294da4d2808cbc53aebd0c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/cf68d225bc43629de4ff54778029aee6dc191b83", - "reference": "cf68d225bc43629de4ff54778029aee6dc191b83", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/0b31a944fcd8759ae294da4d2808cbc53aebd0c3", + "reference": "0b31a944fcd8759ae294da4d2808cbc53aebd0c3", "shasum": "" }, "require": { @@ -8921,7 +9065,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.3.0" + "source": "https://github.com/symfony/error-handler/tree/v7.3.2" }, "funding": [ { @@ -8932,12 +9076,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-29T07:19:49+00:00" + "time": "2025-07-07T08:17:57+00:00" }, { "name": "symfony/event-dispatcher", @@ -9097,16 +9245,16 @@ }, { "name": "symfony/finder", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d" + "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/ec2344cf77a48253bbca6939aa3d2477773ea63d", - "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d", + "url": "https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe", + "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe", "shasum": "" }, "require": { @@ -9141,7 +9289,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.0" + "source": "https://github.com/symfony/finder/tree/v7.3.2" }, "funding": [ { @@ -9152,25 +9300,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-12-30T19:00:26+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "4236baf01609667d53b20371486228231eb135fd" + "reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/4236baf01609667d53b20371486228231eb135fd", - "reference": "4236baf01609667d53b20371486228231eb135fd", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6877c122b3a6cc3695849622720054f6e6fa5fa6", + "reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6", "shasum": "" }, "require": { @@ -9220,7 +9372,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.0" + "source": "https://github.com/symfony/http-foundation/tree/v7.3.2" }, "funding": [ { @@ -9231,25 +9383,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-12T14:48:23+00:00" + "time": "2025-07-10T08:47:49+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "ac7b8e163e8c83dce3abcc055a502d4486051a9f" + "reference": "6ecc895559ec0097e221ed2fd5eb44d5fede083c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/ac7b8e163e8c83dce3abcc055a502d4486051a9f", - "reference": "ac7b8e163e8c83dce3abcc055a502d4486051a9f", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6ecc895559ec0097e221ed2fd5eb44d5fede083c", + "reference": "6ecc895559ec0097e221ed2fd5eb44d5fede083c", "shasum": "" }, "require": { @@ -9334,7 +9490,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.3.0" + "source": "https://github.com/symfony/http-kernel/tree/v7.3.2" }, "funding": [ { @@ -9345,25 +9501,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-29T07:47:32+00:00" + "time": "2025-07-31T10:45:04+00:00" }, { "name": "symfony/mailer", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "0f375bbbde96ae8c78e4aa3e63aabd486e33364c" + "reference": "d43e84d9522345f96ad6283d5dfccc8c1cfc299b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/0f375bbbde96ae8c78e4aa3e63aabd486e33364c", - "reference": "0f375bbbde96ae8c78e4aa3e63aabd486e33364c", + "url": "https://api.github.com/repos/symfony/mailer/zipball/d43e84d9522345f96ad6283d5dfccc8c1cfc299b", + "reference": "d43e84d9522345f96ad6283d5dfccc8c1cfc299b", "shasum": "" }, "require": { @@ -9414,7 +9574,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.3.0" + "source": "https://github.com/symfony/mailer/tree/v7.3.2" }, "funding": [ { @@ -9425,25 +9585,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-04T09:51:09+00:00" + "time": "2025-07-15T11:36:08+00:00" }, { "name": "symfony/mime", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9" + "reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9", - "reference": "0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9", + "url": "https://api.github.com/repos/symfony/mime/zipball/e0a0f859148daf1edf6c60b398eb40bfc96697d1", + "reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1", "shasum": "" }, "require": { @@ -9498,7 +9662,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.3.0" + "source": "https://github.com/symfony/mime/tree/v7.3.2" }, "funding": [ { @@ -9509,25 +9673,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-19T08:51:26+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "afb9a8038025e5dbc657378bfab9198d75f10fca" + "reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/afb9a8038025e5dbc657378bfab9198d75f10fca", - "reference": "afb9a8038025e5dbc657378bfab9198d75f10fca", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/119bcf13e67dbd188e5dbc74228b1686f66acd37", + "reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37", "shasum": "" }, "require": { @@ -9565,7 +9733,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.3.0" + "source": "https://github.com/symfony/options-resolver/tree/v7.3.2" }, "funding": [ { @@ -9576,12 +9744,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-04T13:12:05+00:00" + "time": "2025-07-15T11:36:08+00:00" }, { "name": "symfony/polyfill-ctype", @@ -10446,16 +10618,16 @@ }, { "name": "symfony/routing", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "8e213820c5fea844ecea29203d2a308019007c15" + "reference": "7614b8ca5fa89b9cd233e21b627bfc5774f586e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/8e213820c5fea844ecea29203d2a308019007c15", - "reference": "8e213820c5fea844ecea29203d2a308019007c15", + "url": "https://api.github.com/repos/symfony/routing/zipball/7614b8ca5fa89b9cd233e21b627bfc5774f586e4", + "reference": "7614b8ca5fa89b9cd233e21b627bfc5774f586e4", "shasum": "" }, "require": { @@ -10507,7 +10679,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.3.0" + "source": "https://github.com/symfony/routing/tree/v7.3.2" }, "funding": [ { @@ -10518,12 +10690,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-24T20:43:28+00:00" + "time": "2025-07-15T11:36:08+00:00" }, { "name": "symfony/service-contracts", @@ -10672,16 +10848,16 @@ }, { "name": "symfony/string", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125" + "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125", - "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125", + "url": "https://api.github.com/repos/symfony/string/zipball/42f505aff654e62ac7ac2ce21033818297ca89ca", + "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca", "shasum": "" }, "require": { @@ -10739,7 +10915,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.0" + "source": "https://github.com/symfony/string/tree/v7.3.2" }, "funding": [ { @@ -10750,25 +10926,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-20T20:19:01+00:00" + "time": "2025-07-10T08:47:49+00:00" }, { "name": "symfony/translation", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "4aba29076a29a3aa667e09b791e5f868973a8667" + "reference": "81b48f4daa96272efcce9c7a6c4b58e629df3c90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/4aba29076a29a3aa667e09b791e5f868973a8667", - "reference": "4aba29076a29a3aa667e09b791e5f868973a8667", + "url": "https://api.github.com/repos/symfony/translation/zipball/81b48f4daa96272efcce9c7a6c4b58e629df3c90", + "reference": "81b48f4daa96272efcce9c7a6c4b58e629df3c90", "shasum": "" }, "require": { @@ -10835,7 +11015,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.0" + "source": "https://github.com/symfony/translation/tree/v7.3.2" }, "funding": [ { @@ -10846,12 +11026,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-29T07:19:49+00:00" + "time": "2025-07-30T17:31:46+00:00" }, { "name": "symfony/translation-contracts", @@ -10933,16 +11117,16 @@ }, { "name": "symfony/uid", - "version": "v7.3.0", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "7beeb2b885cd584cd01e126c5777206ae4c3c6a3" + "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/7beeb2b885cd584cd01e126c5777206ae4c3c6a3", - "reference": "7beeb2b885cd584cd01e126c5777206ae4c3c6a3", + "url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb", + "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb", "shasum": "" }, "require": { @@ -10987,7 +11171,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.3.0" + "source": "https://github.com/symfony/uid/tree/v7.3.1" }, "funding": [ { @@ -11003,20 +11187,20 @@ "type": "tidelift" } ], - "time": "2025-05-24T14:28:13+00:00" + "time": "2025-06-27T19:55:54+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e" + "reference": "53205bea27450dc5c65377518b3275e126d45e75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/548f6760c54197b1084e1e5c71f6d9d523f2f78e", - "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/53205bea27450dc5c65377518b3275e126d45e75", + "reference": "53205bea27450dc5c65377518b3275e126d45e75", "shasum": "" }, "require": { @@ -11028,7 +11212,6 @@ "symfony/console": "<6.4" }, "require-dev": { - "ext-iconv": "*", "symfony/console": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", "symfony/process": "^6.4|^7.0", @@ -11071,7 +11254,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.0" + "source": "https://github.com/symfony/var-dumper/tree/v7.3.2" }, "funding": [ { @@ -11082,25 +11265,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-27T18:39:23+00:00" + "time": "2025-07-29T20:02:46+00:00" }, { "name": "symfony/yaml", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "cea40a48279d58dc3efee8112634cb90141156c2" + "reference": "b8d7d868da9eb0919e99c8830431ea087d6aae30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/cea40a48279d58dc3efee8112634cb90141156c2", - "reference": "cea40a48279d58dc3efee8112634cb90141156c2", + "url": "https://api.github.com/repos/symfony/yaml/zipball/b8d7d868da9eb0919e99c8830431ea087d6aae30", + "reference": "b8d7d868da9eb0919e99c8830431ea087d6aae30", "shasum": "" }, "require": { @@ -11143,7 +11330,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.0" + "source": "https://github.com/symfony/yaml/tree/v7.3.2" }, "funding": [ { @@ -11154,12 +11341,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-04T10:10:33+00:00" + "time": "2025-07-10T08:47:49+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -11810,16 +12001,16 @@ }, { "name": "zircote/swagger-php", - "version": "5.1.3", + "version": "5.1.4", "source": { "type": "git", "url": "https://github.com/zircote/swagger-php.git", - "reference": "b8ba6bd99805c0ae09a38d1b26c1c92820509bd0" + "reference": "471f2e7c24c9508a2ee08df245cab64b62dbf721" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zircote/swagger-php/zipball/b8ba6bd99805c0ae09a38d1b26c1c92820509bd0", - "reference": "b8ba6bd99805c0ae09a38d1b26c1c92820509bd0", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/471f2e7c24c9508a2ee08df245cab64b62dbf721", + "reference": "471f2e7c24c9508a2ee08df245cab64b62dbf721", "shasum": "" }, "require": { @@ -11890,24 +12081,24 @@ ], "support": { "issues": "https://github.com/zircote/swagger-php/issues", - "source": "https://github.com/zircote/swagger-php/tree/5.1.3" + "source": "https://github.com/zircote/swagger-php/tree/5.1.4" }, - "time": "2025-05-20T03:35:10+00:00" + "time": "2025-07-15T23:54:13+00:00" } ], "packages-dev": [ { "name": "barryvdh/laravel-debugbar", - "version": "v3.15.4", + "version": "v3.16.0", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "c0667ea91f7185f1e074402c5788195e96bf8106" + "reference": "f265cf5e38577d42311f1a90d619bcd3740bea23" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/c0667ea91f7185f1e074402c5788195e96bf8106", - "reference": "c0667ea91f7185f1e074402c5788195e96bf8106", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/f265cf5e38577d42311f1a90d619bcd3740bea23", + "reference": "f265cf5e38577d42311f1a90d619bcd3740bea23", "shasum": "" }, "require": { @@ -11915,7 +12106,7 @@ "illuminate/session": "^9|^10|^11|^12", "illuminate/support": "^9|^10|^11|^12", "php": "^8.1", - "php-debugbar/php-debugbar": "~2.1.1", + "php-debugbar/php-debugbar": "~2.2.0", "symfony/finder": "^6|^7" }, "require-dev": { @@ -11935,7 +12126,7 @@ ] }, "branch-alias": { - "dev-master": "3.15-dev" + "dev-master": "3.16-dev" } }, "autoload": { @@ -11967,7 +12158,7 @@ ], "support": { "issues": "https://github.com/barryvdh/laravel-debugbar/issues", - "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.15.4" + "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.16.0" }, "funding": [ { @@ -11979,7 +12170,7 @@ "type": "github" } ], - "time": "2025-04-16T06:32:06+00:00" + "time": "2025-07-14T11:56:43+00:00" }, { "name": "brianium/paratest", @@ -12235,16 +12426,16 @@ }, { "name": "filp/whoops", - "version": "2.18.0", + "version": "2.18.3", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e" + "reference": "59a123a3d459c5a23055802237cb317f609867e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e", - "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e", + "url": "https://api.github.com/repos/filp/whoops/zipball/59a123a3d459c5a23055802237cb317f609867e5", + "reference": "59a123a3d459c5a23055802237cb317f609867e5", "shasum": "" }, "require": { @@ -12294,7 +12485,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.18.0" + "source": "https://github.com/filp/whoops/tree/2.18.3" }, "funding": [ { @@ -12302,7 +12493,7 @@ "type": "github" } ], - "time": "2025-03-15T12:00:00+00:00" + "time": "2025-06-16T00:02:10+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -12357,16 +12548,16 @@ }, { "name": "laravel/dusk", - "version": "v8.3.2", + "version": "v8.3.3", "source": { "type": "git", "url": "https://github.com/laravel/dusk.git", - "reference": "bb701836357bf6f6c6658ef90b5a0f8232affb0f" + "reference": "077d448cd993a08f97bfccf0ea3d6478b3908f7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/dusk/zipball/bb701836357bf6f6c6658ef90b5a0f8232affb0f", - "reference": "bb701836357bf6f6c6658ef90b5a0f8232affb0f", + "url": "https://api.github.com/repos/laravel/dusk/zipball/077d448cd993a08f97bfccf0ea3d6478b3908f7e", + "reference": "077d448cd993a08f97bfccf0ea3d6478b3908f7e", "shasum": "" }, "require": { @@ -12425,22 +12616,22 @@ ], "support": { "issues": "https://github.com/laravel/dusk/issues", - "source": "https://github.com/laravel/dusk/tree/v8.3.2" + "source": "https://github.com/laravel/dusk/tree/v8.3.3" }, - "time": "2025-02-20T14:42:00+00:00" + "time": "2025-06-10T13:59:27+00:00" }, { "name": "laravel/pint", - "version": "v1.22.1", + "version": "v1.24.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "941d1927c5ca420c22710e98420287169c7bcaf7" + "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/941d1927c5ca420c22710e98420287169c7bcaf7", - "reference": "941d1927c5ca420c22710e98420287169c7bcaf7", + "url": "https://api.github.com/repos/laravel/pint/zipball/0345f3b05f136801af8c339f9d16ef29e6b4df8a", + "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a", "shasum": "" }, "require": { @@ -12451,10 +12642,10 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.75.0", - "illuminate/view": "^11.44.7", - "larastan/larastan": "^3.4.0", - "laravel-zero/framework": "^11.36.1", + "friendsofphp/php-cs-fixer": "^3.82.2", + "illuminate/view": "^11.45.1", + "larastan/larastan": "^3.5.0", + "laravel-zero/framework": "^11.45.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^2.3.1", "pestphp/pest": "^2.36.0" @@ -12464,6 +12655,9 @@ ], "type": "project", "autoload": { + "files": [ + "overrides/Runner/Parallel/ProcessFactory.php" + ], "psr-4": { "App\\": "app/", "Database\\Seeders\\": "database/seeders/", @@ -12493,20 +12687,20 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-05-08T08:38:12+00:00" + "time": "2025-07-10T18:09:32+00:00" }, { "name": "laravel/telescope", - "version": "v5.8.0", + "version": "v5.10.2", "source": { "type": "git", "url": "https://github.com/laravel/telescope.git", - "reference": "9be1b8851b8ffe67689e7960135f8b32a966f23d" + "reference": "6d249d93ab06dc147ac62ea02b4272c2e7a24b72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/telescope/zipball/9be1b8851b8ffe67689e7960135f8b32a966f23d", - "reference": "9be1b8851b8ffe67689e7960135f8b32a966f23d", + "url": "https://api.github.com/repos/laravel/telescope/zipball/6d249d93ab06dc147ac62ea02b4272c2e7a24b72", + "reference": "6d249d93ab06dc147ac62ea02b4272c2e7a24b72", "shasum": "" }, "require": { @@ -12560,9 +12754,9 @@ ], "support": { "issues": "https://github.com/laravel/telescope/issues", - "source": "https://github.com/laravel/telescope/tree/v5.8.0" + "source": "https://github.com/laravel/telescope/tree/v5.10.2" }, - "time": "2025-05-26T17:22:18+00:00" + "time": "2025-07-24T05:26:13+00:00" }, { "name": "mockery/mockery", @@ -12649,16 +12843,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.1", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -12697,7 +12891,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -12705,27 +12899,27 @@ "type": "tidelift" } ], - "time": "2025-04-29T12:36:36+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nunomaduro/collision", - "version": "v8.8.0", + "version": "v8.8.2", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "4cf9f3b47afff38b139fb79ce54fc71799022ce8" + "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/4cf9f3b47afff38b139fb79ce54fc71799022ce8", - "reference": "4cf9f3b47afff38b139fb79ce54fc71799022ce8", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", + "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", "shasum": "" }, "require": { - "filp/whoops": "^2.18.0", - "nunomaduro/termwind": "^2.3.0", + "filp/whoops": "^2.18.1", + "nunomaduro/termwind": "^2.3.1", "php": "^8.2.0", - "symfony/console": "^7.2.5" + "symfony/console": "^7.3.0" }, "conflict": { "laravel/framework": "<11.44.2 || >=13.0.0", @@ -12733,15 +12927,15 @@ }, "require-dev": { "brianium/paratest": "^7.8.3", - "larastan/larastan": "^3.2", - "laravel/framework": "^11.44.2 || ^12.6", - "laravel/pint": "^1.21.2", - "laravel/sail": "^1.41.0", - "laravel/sanctum": "^4.0.8", + "larastan/larastan": "^3.4.2", + "laravel/framework": "^11.44.2 || ^12.18", + "laravel/pint": "^1.22.1", + "laravel/sail": "^1.43.1", + "laravel/sanctum": "^4.1.1", "laravel/tinker": "^2.10.1", - "orchestra/testbench-core": "^9.12.0 || ^10.1", - "pestphp/pest": "^3.8.0", - "sebastian/environment": "^7.2.0 || ^8.0" + "orchestra/testbench-core": "^9.12.0 || ^10.4", + "pestphp/pest": "^3.8.2", + "sebastian/environment": "^7.2.1 || ^8.0" }, "type": "library", "extra": { @@ -12804,7 +12998,7 @@ "type": "patreon" } ], - "time": "2025-04-03T14:33:09+00:00" + "time": "2025-06-25T02:12:12+00:00" }, { "name": "pestphp/pest", @@ -13250,16 +13444,16 @@ }, { "name": "php-debugbar/php-debugbar", - "version": "v2.1.6", + "version": "v2.2.4", "source": { "type": "git", "url": "https://github.com/php-debugbar/php-debugbar.git", - "reference": "16fa68da5617220594aa5e33fa9de415f94784a0" + "reference": "3146d04671f51f69ffec2a4207ac3bdcf13a9f35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-debugbar/php-debugbar/zipball/16fa68da5617220594aa5e33fa9de415f94784a0", - "reference": "16fa68da5617220594aa5e33fa9de415f94784a0", + "url": "https://api.github.com/repos/php-debugbar/php-debugbar/zipball/3146d04671f51f69ffec2a4207ac3bdcf13a9f35", + "reference": "3146d04671f51f69ffec2a4207ac3bdcf13a9f35", "shasum": "" }, "require": { @@ -13267,6 +13461,9 @@ "psr/log": "^1|^2|^3", "symfony/var-dumper": "^4|^5|^6|^7" }, + "replace": { + "maximebf/debugbar": "self.version" + }, "require-dev": { "dbrekelmans/bdi": "^1", "phpunit/phpunit": "^8|^9", @@ -13281,7 +13478,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -13314,9 +13511,9 @@ ], "support": { "issues": "https://github.com/php-debugbar/php-debugbar/issues", - "source": "https://github.com/php-debugbar/php-debugbar/tree/v2.1.6" + "source": "https://github.com/php-debugbar/php-debugbar/tree/v2.2.4" }, - "time": "2025-02-21T17:47:03+00:00" + "time": "2025-07-22T14:01:30+00:00" }, { "name": "php-webdriver/webdriver", @@ -13386,16 +13583,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.17", + "version": "2.1.21", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053" + "reference": "1ccf445757458c06a04eb3f803603cb118fe5fa6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053", - "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1ccf445757458c06a04eb3f803603cb118fe5fa6", + "reference": "1ccf445757458c06a04eb3f803603cb118fe5fa6", "shasum": "" }, "require": { @@ -13440,20 +13637,20 @@ "type": "github" } ], - "time": "2025-05-21T20:55:28+00:00" + "time": "2025-07-28T19:35:08+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "11.0.9", + "version": "11.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7" + "reference": "1a800a7446add2d79cc6b3c01c45381810367d76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/14d63fbcca18457e49c6f8bebaa91a87e8e188d7", - "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1a800a7446add2d79cc6b3c01c45381810367d76", + "reference": "1a800a7446add2d79cc6b3c01c45381810367d76", "shasum": "" }, "require": { @@ -13510,15 +13707,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.9" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/show" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" } ], - "time": "2025-02-25T13:26:39+00:00" + "time": "2025-06-18T08:56:18+00:00" }, { "name": "phpunit/php-file-iterator", @@ -13868,21 +14077,21 @@ }, { "name": "rector/rector", - "version": "2.0.16", + "version": "2.1.2", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "f1366d1f8c7490541c8f7af6e5c7cef7cca1b5a2" + "reference": "40a71441dd73fa150a66102f5ca1364c44fc8fff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/f1366d1f8c7490541c8f7af6e5c7cef7cca1b5a2", - "reference": "f1366d1f8c7490541c8f7af6e5c7cef7cca1b5a2", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/40a71441dd73fa150a66102f5ca1364c44fc8fff", + "reference": "40a71441dd73fa150a66102f5ca1364c44fc8fff", "shasum": "" }, "require": { "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.14" + "phpstan/phpstan": "^2.1.18" }, "conflict": { "rector/rector-doctrine": "*", @@ -13907,6 +14116,7 @@ "MIT" ], "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "homepage": "https://getrector.com/", "keywords": [ "automation", "dev", @@ -13915,7 +14125,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.0.16" + "source": "https://github.com/rectorphp/rector/tree/2.1.2" }, "funding": [ { @@ -13923,7 +14133,7 @@ "type": "github" } ], - "time": "2025-05-12T16:37:16+00:00" + "time": "2025-07-17T19:30:06+00:00" }, { "name": "sebastian/cli-parser", @@ -15279,16 +15489,16 @@ }, { "name": "symfony/http-client", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "57e4fb86314015a695a750ace358d07a7e37b8a9" + "reference": "1c064a0c67749923483216b081066642751cc2c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/57e4fb86314015a695a750ace358d07a7e37b8a9", - "reference": "57e4fb86314015a695a750ace358d07a7e37b8a9", + "url": "https://api.github.com/repos/symfony/http-client/zipball/1c064a0c67749923483216b081066642751cc2c7", + "reference": "1c064a0c67749923483216b081066642751cc2c7", "shasum": "" }, "require": { @@ -15300,6 +15510,7 @@ }, "conflict": { "amphp/amp": "<2.5", + "amphp/socket": "<1.1", "php-http/discovery": "<1.15", "symfony/http-foundation": "<6.4" }, @@ -15312,7 +15523,6 @@ "require-dev": { "amphp/http-client": "^4.2.1|^5.0", "amphp/http-tunnel": "^1.0|^2.0", - "amphp/socket": "^1.1", "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", @@ -15354,7 +15564,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.3.0" + "source": "https://github.com/symfony/http-client/tree/v7.3.2" }, "funding": [ { @@ -15365,12 +15575,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-02T08:23:16+00:00" + "time": "2025-07-15T11:36:08+00:00" }, { "name": "symfony/http-client-contracts", diff --git a/config/api.php b/config/api.php new file mode 100644 index 000000000..83bac8f03 --- /dev/null +++ b/config/api.php @@ -0,0 +1,5 @@ + env('API_RATE_LIMIT', 200), +]; diff --git a/config/constants.php b/config/constants.php index 78d77d78e..c7a36d311 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,9 +2,9 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.420', - 'helper_version' => '1.0.8', - 'realtime_version' => '1.0.9', + 'version' => '4.0.0-beta.420.7', + 'helper_version' => '1.0.9', + 'realtime_version' => '1.0.10', 'self_hosted' => env('SELF_HOSTED', true), 'autoupdate' => env('AUTOUPDATE'), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), diff --git a/config/horizon.php b/config/horizon.php index 6086b30da..cdabcb1e8 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -182,14 +182,15 @@ 'defaults' => [ 's6' => [ 'connection' => 'redis', - 'queue' => ['high', 'default'], - 'balance' => env('HORIZON_BALANCE', 'auto'), - 'maxTime' => 0, - 'maxJobs' => 0, + 'balance' => env('HORIZON_BALANCE', 'false'), + 'queue' => env('HORIZON_QUEUES', 'high,default'), + 'maxTime' => 3600, + 'maxJobs' => 400, 'memory' => 128, 'tries' => 1, - 'timeout' => 3560, 'nice' => 0, + 'sleep' => 3, + 'timeout' => 3600, ], ], @@ -198,7 +199,7 @@ 's6' => [ 'autoScalingStrategy' => 'size', 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1), - 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6), + 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 4), 'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1), 'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1), ], @@ -208,7 +209,7 @@ 's6' => [ 'autoScalingStrategy' => 'size', 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1), - 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6), + 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 4), 'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1), 'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1), ], diff --git a/config/logging.php b/config/logging.php index 4c3df4ce1..488327414 100644 --- a/config/logging.php +++ b/config/logging.php @@ -118,6 +118,20 @@ 'emergency' => [ 'path' => storage_path('logs/laravel.log'), ], + + 'scheduled' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/scheduled.log'), + 'level' => 'debug', + 'days' => 1, + ], + + 'scheduled-errors' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/scheduled-errors.log'), + 'level' => 'debug', + 'days' => 7, + ], ], ]; diff --git a/config/services.php b/config/services.php index d1c4a3699..7add50a5c 100644 --- a/config/services.php +++ b/config/services.php @@ -46,6 +46,13 @@ 'redirect' => env('AUTHENTIK_REDIRECT_URI'), ], + 'clerk' => [ + 'client_id' => env('CLERK_CLIENT_ID'), + 'client_secret' => env('CLERK_CLIENT_SECRET'), + 'redirect' => env('CLERK_REDIRECT_URI'), + 'base_url' => env('CLERK_BASE_URL'), + ], + 'google' => [ 'client_id' => env('GOOGLE_CLIENT_ID'), 'client_secret' => env('GOOGLE_CLIENT_SECRET'), @@ -53,4 +60,11 @@ 'tenant' => env('GOOGLE_TENANT'), ], + 'zitadel' => [ + 'client_id' => env('ZITADEL_CLIENT_ID'), + 'client_secret' => env('ZITADEL_CLIENT_SECRET'), + 'redirect' => env('ZITADEL_REDIRECT_URI'), + 'base_url' => env('ZITADEL_BASE_URL'), + ] + ]; diff --git a/database/migrations/2025_06_25_131350_add_is_sponsorship_popup_enabled_to_instance_settings_table.php b/database/migrations/2025_06_25_131350_add_is_sponsorship_popup_enabled_to_instance_settings_table.php new file mode 100644 index 000000000..7307da953 --- /dev/null +++ b/database/migrations/2025_06_25_131350_add_is_sponsorship_popup_enabled_to_instance_settings_table.php @@ -0,0 +1,28 @@ +boolean('is_sponsorship_popup_enabled')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('instance_settings', function (Blueprint $table) { + $table->dropColumn('is_sponsorship_popup_enabled'); + }); + } +}; diff --git a/database/migrations/2025_06_26_131350_optimize_activity_log_indexes.php b/database/migrations/2025_06_26_131350_optimize_activity_log_indexes.php new file mode 100644 index 000000000..6ffe97c07 --- /dev/null +++ b/database/migrations/2025_06_26_131350_optimize_activity_log_indexes.php @@ -0,0 +1,38 @@ +>\'type_uuid\'), created_at DESC)'); + + // Add specific index for status queries on properties + DB::statement('CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_activity_properties_status ON activity_log ((properties->>\'status\'))'); + + } catch (\Exception $e) { + Log::error('Error adding optimized indexes to activity_log: '.$e->getMessage()); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + try { + DB::statement('DROP INDEX CONCURRENTLY IF EXISTS idx_activity_type_uuid_created_at'); + DB::statement('DROP INDEX CONCURRENTLY IF EXISTS idx_activity_properties_status'); + } catch (\Exception $e) { + Log::error('Error dropping optimized indexes from activity_log: '.$e->getMessage()); + } + } +}; diff --git a/database/migrations/2025_07_14_191016_add_deleted_at_to_application_previews_table.php b/database/migrations/2025_07_14_191016_add_deleted_at_to_application_previews_table.php new file mode 100644 index 000000000..25aa0f5f0 --- /dev/null +++ b/database/migrations/2025_07_14_191016_add_deleted_at_to_application_previews_table.php @@ -0,0 +1,28 @@ +softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_previews', function (Blueprint $table) { + $table->dropSoftDeletes(); + }); + } +}; diff --git a/database/migrations/2025_07_16_202201_add_timeout_to_scheduled_database_backups_table.php b/database/migrations/2025_07_16_202201_add_timeout_to_scheduled_database_backups_table.php new file mode 100644 index 000000000..f8f8cb8ad --- /dev/null +++ b/database/migrations/2025_07_16_202201_add_timeout_to_scheduled_database_backups_table.php @@ -0,0 +1,18 @@ +integer('timeout')->default(3600); + }); + } +}; diff --git a/database/seeders/OauthSettingSeeder.php b/database/seeders/OauthSettingSeeder.php index fa692d2dc..2e5e6fcc4 100644 --- a/database/seeders/OauthSettingSeeder.php +++ b/database/seeders/OauthSettingSeeder.php @@ -17,11 +17,14 @@ public function run(): void $providers = collect([ 'azure', 'bitbucket', + 'clerk', + 'discord', 'github', 'gitlab', 'google', 'authentik', 'infomaniak', + 'zitadel', ]); $isOauthSeeded = OauthSetting::count() > 0; diff --git a/docker/coolify-helper/Dockerfile b/docker/coolify-helper/Dockerfile index b62469cef..8c7073519 100644 --- a/docker/coolify-helper/Dockerfile +++ b/docker/coolify-helper/Dockerfile @@ -4,15 +4,15 @@ ARG BASE_IMAGE=alpine:3.21 # https://download.docker.com/linux/static/stable/ ARG DOCKER_VERSION=28.0.0 # https://github.com/docker/compose/releases -ARG DOCKER_COMPOSE_VERSION=2.34.0 +ARG DOCKER_COMPOSE_VERSION=2.38.2 # https://github.com/docker/buildx/releases -ARG DOCKER_BUILDX_VERSION=0.22.0 +ARG DOCKER_BUILDX_VERSION=0.25.0 # https://github.com/buildpacks/pack/releases -ARG PACK_VERSION=0.37.0 +ARG PACK_VERSION=0.38.2 # https://github.com/railwayapp/nixpacks/releases -ARG NIXPACKS_VERSION=1.34.1 +ARG NIXPACKS_VERSION=1.39.0 # https://github.com/minio/mc/releases -ARG MINIO_VERSION=RELEASE.2025-03-12T17-29-24Z +ARG MINIO_VERSION=RELEASE.2025-05-21T01-59-54Z FROM minio/mc:${MINIO_VERSION} AS minio-client diff --git a/docker/coolify-realtime/Dockerfile b/docker/coolify-realtime/Dockerfile index 7a24200d6..18c2f9301 100644 --- a/docker/coolify-realtime/Dockerfile +++ b/docker/coolify-realtime/Dockerfile @@ -2,7 +2,7 @@ # https://github.com/soketi/soketi/releases ARG SOKETI_VERSION=1.6-16-alpine # https://github.com/cloudflare/cloudflared/releases -ARG CLOUDFLARED_VERSION=2025.5.0 +ARG CLOUDFLARED_VERSION=2025.7.0 FROM quay.io/soketi/soketi:${SOKETI_VERSION} diff --git a/docker/coolify-realtime/package-lock.json b/docker/coolify-realtime/package-lock.json index 1c329e47f..49907cbd4 100644 --- a/docker/coolify-realtime/package-lock.json +++ b/docker/coolify-realtime/package-lock.json @@ -181,14 +181,15 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -323,9 +324,9 @@ } }, "node_modules/nan": { - "version": "2.22.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.1.tgz", - "integrity": "sha512-pfRR4ZcNTSm2ZFHaztuvbICf+hyiG6ecA06SfAxoPmuHjvMu0KUIae7Y8GyVkbBqeEIidsmXeYooWIX9+qjfRQ==", + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", + "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", "license": "MIT" }, "node_modules/node-pty": { diff --git a/docker/development/Dockerfile b/docker/development/Dockerfile index 8c5beec07..85cce14d7 100644 --- a/docker/development/Dockerfile +++ b/docker/development/Dockerfile @@ -2,9 +2,9 @@ # https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-nginx-alpine ARG SERVERSIDEUP_PHP_VERSION=8.4-fpm-nginx-alpine # https://github.com/minio/mc/releases -ARG MINIO_VERSION=RELEASE.2025-03-12T17-29-24Z +ARG MINIO_VERSION=RELEASE.2025-05-21T01-59-54Z # https://github.com/cloudflare/cloudflared/releases -ARG CLOUDFLARED_VERSION=2025.2.0 +ARG CLOUDFLARED_VERSION=2025.7.0 # https://www.postgresql.org/support/versioning/ ARG POSTGRES_VERSION=15 diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile index 5633170e3..a2a4b5fa3 100644 --- a/docker/production/Dockerfile +++ b/docker/production/Dockerfile @@ -2,9 +2,9 @@ # https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-nginx-alpine ARG SERVERSIDEUP_PHP_VERSION=8.4-fpm-nginx-alpine # https://github.com/minio/mc/releases -ARG MINIO_VERSION=RELEASE.2025-03-12T17-29-24Z +ARG MINIO_VERSION=RELEASE.2025-05-21T01-59-54Z # https://github.com/cloudflare/cloudflared/releases -ARG CLOUDFLARED_VERSION=2025.2.0 +ARG CLOUDFLARED_VERSION=2025.7.0 # https://www.postgresql.org/support/versioning/ ARG POSTGRES_VERSION=15 diff --git a/docker/testing-host/Dockerfile b/docker/testing-host/Dockerfile index b19d0875c..fdad3cc41 100644 --- a/docker/testing-host/Dockerfile +++ b/docker/testing-host/Dockerfile @@ -2,9 +2,9 @@ # https://download.docker.com/linux/static/stable/ ARG DOCKER_VERSION=28.0.0 # https://github.com/docker/compose/releases -ARG DOCKER_COMPOSE_VERSION=2.34.0 +ARG DOCKER_COMPOSE_VERSION=2.38.2 # https://github.com/docker/buildx/releases -ARG DOCKER_BUILDX_VERSION=0.22.0 +ARG DOCKER_BUILDX_VERSION=0.25.0 FROM debian:12-slim diff --git a/lang/ar.json b/lang/ar.json index 9befd1e4a..c966cc686 100644 --- a/lang/ar.json +++ b/lang/ar.json @@ -3,13 +3,16 @@ "auth.login.authentik": "تسجيل الدخول باستخدام Authentik", "auth.login.azure": "تسجيل الدخول باستخدام Microsoft", "auth.login.bitbucket": "تسجيل الدخول باستخدام Bitbucket", + "auth.login.clerk": "تسجيل الدخول باستخدام Clerk", + "auth.login.discord": "تسجيل الدخول باستخدام Discord", "auth.login.github": "تسجيل الدخول باستخدام GitHub", "auth.login.gitlab": "تسجيل الدخول باستخدام Gitlab", "auth.login.google": "تسجيل الدخول باستخدام Google", "auth.login.infomaniak": "تسجيل الدخول باستخدام Infomaniak", "auth.already_registered": "هل سبق لك التسجيل؟", "auth.confirm_password": "تأكيد كلمة المرور", - "auth.forgot_password": "نسيت كلمة المرور", + "auth.forgot_password_link": "هل نسيت كلمة المرور؟", + "auth.forgot_password_heading": "استعادة كلمة المرور", "auth.forgot_password_send_email": "إرسال بريد إلكتروني لإعادة تعيين كلمة المرور", "auth.register_now": "تسجيل", "auth.logout": "تسجيل الخروج", @@ -37,4 +40,4 @@ "resource.delete_configurations": "حذف جميع ملفات التعريف من الخادم بشكل دائم.", "database.delete_backups_locally": "حذف كافة النسخ الاحتياطية نهائيًا من التخزين المحلي.", "warning.sslipdomain": "تم حفظ ملفات التعريف الخاصة بك، ولكن استخدام نطاق sslip مع https غير مستحسن، لأن خوادم Let's Encrypt مع هذا النطاق العام محدودة المعدل (ستفشل عملية التحقق من شهادة SSL).

استخدم نطاقك الخاص بدلاً من ذلك." -} +} \ No newline at end of file diff --git a/lang/az.json b/lang/az.json index 973c70c2f..85cee7589 100644 --- a/lang/az.json +++ b/lang/az.json @@ -3,13 +3,16 @@ "auth.login.authentik": "Authentik ilə daxil ol", "auth.login.azure": "Azure ilə daxil ol", "auth.login.bitbucket": "Bitbucket ilə daxil ol", + "auth.login.clerk": "Clerk ilə daxil ol", + "auth.login.discord": "Discord ilə daxil ol", "auth.login.github": "Github ilə daxil ol", "auth.login.gitlab": "GitLab ilə daxil ol", "auth.login.google": "Google ilə daxil ol", "auth.login.infomaniak": "Infomaniak ilə daxil ol", "auth.already_registered": "Qeytiyatınız var?", "auth.confirm_password": "Şifrəni təsdiqləyin", - "auth.forgot_password": "Şifrəmi unutdum", + "auth.forgot_password_link": "Şifrəmi unutdum?", + "auth.forgot_password_heading": "Şifrəni bərpa et", "auth.forgot_password_send_email": "Şifrəni sıfırlamaq üçün e-poçt göndər", "auth.register_now": "Qeydiyyat", "auth.logout": "Çıxış", @@ -37,4 +40,4 @@ "resource.delete_configurations": "Serverdən bütün konfiqurasiya faylları tamamilə silinəcək.", "database.delete_backups_locally": "Bütün ehtiyat nüsxələr lokal yaddaşdan tamamilə silinəcək.", "warning.sslipdomain": "Konfiqurasiya yadda saxlanıldı, lakin sslip domeni ilə https TÖVSİYƏ EDİLMİR, çünki Let's Encrypt serverləri bu ümumi domenlə məhdudlaşdırılır (SSL sertifikatının təsdiqlənməsi uğursuz olacaq).

Əvəzində öz domeninizdən istifadə edin." -} +} \ No newline at end of file diff --git a/lang/cs.json b/lang/cs.json index 270fd272b..9e5d2c44e 100644 --- a/lang/cs.json +++ b/lang/cs.json @@ -2,13 +2,16 @@ "auth.login": "Přihlásit se", "auth.login.azure": "Přihlásit se pomocí Microsoftu", "auth.login.bitbucket": "Přihlásit se pomocí Bitbucketu", + "auth.login.clerk": "Přihlásit se pomocí Clerk", + "auth.login.discord": "Přihlásit se pomocí Discordu", "auth.login.github": "Přihlásit se pomocí GitHubu", "auth.login.gitlab": "Přihlásit se pomocí Gitlabu", "auth.login.google": "Přihlásit se pomocí Google", "auth.login.infomaniak": "Přihlásit se pomocí Infomaniak", "auth.already_registered": "Již jste registrováni?", "auth.confirm_password": "Potvrďte heslo", - "auth.forgot_password": "Zapomněli jste heslo", + "auth.forgot_password_link": "Zapomněli jste heslo?", + "auth.forgot_password_heading": "Obnovení hesla", "auth.forgot_password_send_email": "Poslat e-mail pro resetování hesla", "auth.register_now": "Registrovat se", "auth.logout": "Odhlásit se", @@ -28,4 +31,4 @@ "input.recovery_code": "Obnovovací kód", "button.save": "Uložit", "repository.url": "Příklady
Pro veřejné repozitáře, použijte https://....
Pro soukromé repozitáře, použijte git@....

https://github.com/coollabsio/coolify-examples main branch bude zvolena
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify branch bude vybrána.
https://gitea.com/sedlav/expressjs.git main branch vybrána.
https://gitlab.com/andrasbacsai/nodejs-example.git main branch bude vybrána." -} +} \ No newline at end of file diff --git a/lang/de.json b/lang/de.json index c5644e3a7..fd587de22 100644 --- a/lang/de.json +++ b/lang/de.json @@ -2,13 +2,17 @@ "auth.login": "Anmelden", "auth.login.azure": "Mit Microsoft anmelden", "auth.login.bitbucket": "Mit Bitbucket anmelden", + "auth.login.clerk": "Mit Clerk anmelden", + "auth.login.discord": "Mit Discord anmelden", "auth.login.github": "Mit GitHub anmelden", "auth.login.gitlab": "Mit GitLab anmelden", "auth.login.google": "Mit Google anmelden", "auth.login.infomaniak": "Mit Infomaniak anmelden", + "auth.login.zitadel": "Mit Zitadel anmelden", "auth.already_registered": "Bereits registriert?", "auth.confirm_password": "Passwort bestätigen", - "auth.forgot_password": "Passwort vergessen", + "auth.forgot_password_link": "Passwort vergessen?", + "auth.forgot_password_heading": "Passwort-Wiederherstellung", "auth.forgot_password_send_email": "Passwort zurücksetzen E-Mail senden", "auth.register_now": "Registrieren", "auth.logout": "Abmelden", @@ -28,4 +32,4 @@ "input.recovery_code": "Wiederherstellungscode", "button.save": "Speichern", "repository.url": "Beispiele
Für öffentliche Repositories benutze https://....
Für private Repositories benutze git@....

https://github.com/coollabsio/coolify-examples main Branch wird ausgewählt
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify Branch wird ausgewählt.
https://gitea.com/sedlav/expressjs.git main Branch wird ausgewählt.
https://gitlab.com/andrasbacsai/nodejs-example.git main Branch wird ausgewählt." -} +} \ No newline at end of file diff --git a/lang/en.json b/lang/en.json index cdca68601..af7f2145d 100644 --- a/lang/en.json +++ b/lang/en.json @@ -3,13 +3,17 @@ "auth.login.authentik": "Login with Authentik", "auth.login.azure": "Login with Microsoft", "auth.login.bitbucket": "Login with Bitbucket", + "auth.login.clerk": "Login with Clerk", + "auth.login.discord": "Login with Discord", "auth.login.github": "Login with GitHub", "auth.login.gitlab": "Login with Gitlab", "auth.login.google": "Login with Google", "auth.login.infomaniak": "Login with Infomaniak", + "auth.login.zitadel": "Login with Zitadel", "auth.already_registered": "Already registered?", "auth.confirm_password": "Confirm password", - "auth.forgot_password": "Forgot password", + "auth.forgot_password_link": "Forgot password?", + "auth.forgot_password_heading": "Password recovery", "auth.forgot_password_send_email": "Send password reset email", "auth.register_now": "Register", "auth.logout": "Logout", @@ -37,4 +41,4 @@ "resource.delete_configurations": "Permanently delete all configuration files from the server.", "database.delete_backups_locally": "All backups will be permanently deleted from local storage.", "warning.sslipdomain": "Your configuration is saved, but sslip domain with https is NOT recommended, because Let's Encrypt servers with this public domain are rate limited (SSL certificate validation will fail).

Use your own domain instead." -} +} \ No newline at end of file diff --git a/lang/es.json b/lang/es.json index aceacd462..f56387f05 100644 --- a/lang/es.json +++ b/lang/es.json @@ -2,13 +2,16 @@ "auth.login": "Iniciar Sesión", "auth.login.azure": "Acceder con Microsoft", "auth.login.bitbucket": "Acceder con Bitbucket", + "auth.login.clerk": "Acceder con Clerk", + "auth.login.discord": "Acceder con Discord", "auth.login.github": "Acceder con GitHub", "auth.login.gitlab": "Acceder con Gitlab", "auth.login.google": "Acceder con Google", "auth.login.infomaniak": "Acceder con Infomaniak", "auth.already_registered": "¿Ya estás registrado?", "auth.confirm_password": "Confirmar contraseña", - "auth.forgot_password": "¿Olvidaste tu contraseña?", + "auth.forgot_password_link": "¿Olvidaste tu contraseña?", + "auth.forgot_password_heading": "Recuperación de contraseña", "auth.forgot_password_send_email": "Enviar correo de recuperación de contraseña", "auth.register_now": "Registrar", "auth.logout": "Cerrar sesión", @@ -28,4 +31,4 @@ "input.recovery_code": "Código de recuperación", "button.save": "Guardar", "repository.url": "Examples
Para repositorios públicos, usar https://....
Para repositorios privados, usar git@....

https://github.com/coollabsio/coolify-examples main la rama 'main' será seleccionada.
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify la rama 'nodejs-fastify' será seleccionada.
https://gitea.com/sedlav/expressjs.git main la rama 'main' será seleccionada.
https://gitlab.com/andrasbacsai/nodejs-example.git main la rama 'main' será seleccionada." -} +} \ No newline at end of file diff --git a/lang/fa.json b/lang/fa.json index 7a714e626..ae22ee946 100644 --- a/lang/fa.json +++ b/lang/fa.json @@ -2,13 +2,16 @@ "auth.login": "ورود", "auth.login.azure": "ورود با مایکروسافت", "auth.login.bitbucket": "ورود با Bitbucket", + "auth.login.clerk": "ورود با Clerk", + "auth.login.discord": "ورود با Discord", "auth.login.github": "ورود با گیت هاب", "auth.login.gitlab": "ورود با گیت لب", "auth.login.google": "ورود با گوگل", "auth.login.infomaniak": "ورود با Infomaniak", "auth.already_registered": "قبلاً ثبت نام کرده‌اید؟", "auth.confirm_password": "تایید رمز عبور", - "auth.forgot_password": "فراموشی رمز عبور", + "auth.forgot_password_link": "رمز عبور را فراموش کرده‌اید؟", + "auth.forgot_password_heading": "بازیابی رمز عبور", "auth.forgot_password_send_email": "ارسال ایمیل بازیابی رمز عبور", "auth.register_now": "ثبت نام", "auth.logout": "خروج", @@ -28,4 +31,4 @@ "input.recovery_code": "کد بازیابی", "button.save": "ذخیره", "repository.url": "مثال‌ها
برای مخازن عمومی، از https://... استفاده کنید.
برای مخازن خصوصی، از git@... استفاده کنید.

شاخه main انتخاب خواهد شد.
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify شاخه nodejs-fastify انتخاب خواهد شد.
https://gitea.com/sedlav/expressjs.git شاخه main انتخاب خواهد شد.
https://gitlab.com/andrasbacsai/nodejs-example.git شاخه main انتخاب خواهد شد." -} +} \ No newline at end of file diff --git a/lang/fr.json b/lang/fr.json index 68763d5e0..d98a1ebc8 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -3,13 +3,16 @@ "auth.login.authentik": "Connexion avec Authentik", "auth.login.azure": "Connexion avec Microsoft", "auth.login.bitbucket": "Connexion avec Bitbucket", + "auth.login.clerk": "Connexion avec Clerk", + "auth.login.discord": "Connexion avec Discord", "auth.login.github": "Connexion avec GitHub", "auth.login.gitlab": "Connexion avec Gitlab", "auth.login.google": "Connexion avec Google", "auth.login.infomaniak": "Connexion avec Infomaniak", "auth.already_registered": "Déjà enregistré ?", "auth.confirm_password": "Confirmer le mot de passe", - "auth.forgot_password": "Mot de passe oublié", + "auth.forgot_password_link": "Mot de passe oublié ?", + "auth.forgot_password_heading": "Récupération du mot de passe", "auth.forgot_password_send_email": "Envoyer l'email de réinitialisation de mot de passe", "auth.register_now": "S'enregistrer", "auth.logout": "Déconnexion", @@ -37,4 +40,4 @@ "resource.delete_configurations": "Supprimer définitivement tous les fichiers de configuration du serveur.", "database.delete_backups_locally": "Toutes les sauvegardes seront définitivement supprimées du stockage local.", "warning.sslipdomain": "Votre configuration est enregistrée, mais l'utilisation du domaine sslip avec https N'EST PAS recommandée, car les serveurs Let's Encrypt avec ce domaine public sont limités en taux (la validation du certificat SSL échouera).

Utilisez plutôt votre propre domaine." -} +} \ No newline at end of file diff --git a/lang/id.json b/lang/id.json index d35556402..d85176cda 100644 --- a/lang/id.json +++ b/lang/id.json @@ -3,13 +3,16 @@ "auth.login.authentik": "Masuk dengan Authentik", "auth.login.azure": "Masuk dengan Microsoft", "auth.login.bitbucket": "Masuk dengan Bitbucket", + "auth.login.clerk": "Masuk dengan Clerk", + "auth.login.discord": "Masuk dengan Discord", "auth.login.github": "Masuk dengan GitHub", "auth.login.gitlab": "Masuk dengan Gitlab", "auth.login.google": "Masuk dengan Google", "auth.login.infomaniak": "Masuk dengan Infomaniak", "auth.already_registered": "Sudah terdaftar?", "auth.confirm_password": "Konfirmasi kata sandi", - "auth.forgot_password": "Lupa kata sandi", + "auth.forgot_password_link": "Lupa kata sandi?", + "auth.forgot_password_heading": "Pemulihan Kata Sandi", "auth.forgot_password_send_email": "Kirim email reset kata sandi", "auth.register_now": "Daftar", "auth.logout": "Keluar", @@ -37,4 +40,4 @@ "resource.delete_configurations": "Hapus permanen semua file konfigurasi dari server.", "database.delete_backups_locally": "Semua backup akan dihapus permanen dari penyimpanan lokal.", "warning.sslipdomain": "Konfigurasi Anda disimpan, tetapi domain sslip dengan https TIDAK direkomendasikan, karena server Let's Encrypt dengan domain publik ini dibatasi (validasi sertifikat SSL akan gagal).

Gunakan domain Anda sendiri sebagai gantinya." -} +} \ No newline at end of file diff --git a/lang/it.json b/lang/it.json index 1923251a5..e4c1a9c05 100644 --- a/lang/it.json +++ b/lang/it.json @@ -3,13 +3,16 @@ "auth.login.authentik": "Accedi con Authentik", "auth.login.azure": "Accedi con Microsoft", "auth.login.bitbucket": "Accedi con Bitbucket", + "auth.login.clerk": "Accedi con Clerk", + "auth.login.discord": "Accedi con Discord", "auth.login.github": "Accedi con GitHub", "auth.login.gitlab": "Accedi con Gitlab", "auth.login.google": "Accedi con Google", "auth.login.infomaniak": "Accedi con Infomaniak", "auth.already_registered": "Già registrato?", "auth.confirm_password": "Conferma password", - "auth.forgot_password": "Password dimenticata", + "auth.forgot_password_link": "Hai dimenticato la password?", + "auth.forgot_password_heading": "Recupero password", "auth.forgot_password_send_email": "Invia email per reimpostare la password", "auth.register_now": "Registrati", "auth.logout": "Esci", @@ -37,4 +40,4 @@ "resource.delete_configurations": "Elimina definitivamente tutti i file di configurazione dal server.", "database.delete_backups_locally": "Tutti i backup verranno eliminati definitivamente dall'archiviazione locale.", "warning.sslipdomain": "La tua configurazione è stata salvata, ma il dominio sslip con https NON è raccomandato, poiché i server di Let's Encrypt con questo dominio pubblico hanno limitazioni di frequenza (la convalida del certificato SSL fallirà).

Utilizza invece il tuo dominio personale." -} +} \ No newline at end of file diff --git a/lang/ja.json b/lang/ja.json index 4d4589900..05987e7ce 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -2,13 +2,16 @@ "auth.login": "ログイン", "auth.login.azure": "Microsoftでログイン", "auth.login.bitbucket": "Bitbucketでログイン", + "auth.login.clerk": "Clerkでログイン", + "auth.login.discord": "Discordでログイン", "auth.login.github": "GitHubでログイン", "auth.login.gitlab": "Gitlabでログイン", "auth.login.google": "Googleでログイン", "auth.login.infomaniak": "Infomaniakでログイン", "auth.already_registered": "すでに登録済みですか?", "auth.confirm_password": "パスワードを確認", - "auth.forgot_password": "パスワードを忘れた", + "auth.forgot_password_link": "パスワードをお忘れですか?", + "auth.forgot_password_heading": "パスワードの再設定", "auth.forgot_password_send_email": "パスワードリセットメールを送信", "auth.register_now": "今すぐ登録", "auth.logout": "ログアウト", @@ -28,4 +31,4 @@ "input.recovery_code": "リカバリーコード", "button.save": "保存", "repository.url": "
公開リポジトリの場合はhttps://...を使用してください。
プライベートリポジトリの場合はgit@...を使用してください。

https://github.com/coollabsio/coolify-examples mainブランチが選択されます
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastifyブランチが選択されます。
https://gitea.com/sedlav/expressjs.git mainブランチが選択されます。
https://gitlab.com/andrasbacsai/nodejs-example.git mainブランチが選択されます。" -} +} \ No newline at end of file diff --git a/lang/no.json b/lang/no.json index 29d5af124..967bdf606 100644 --- a/lang/no.json +++ b/lang/no.json @@ -3,13 +3,16 @@ "auth.login.authentik": "Logg inn med Authentik", "auth.login.azure": "Logg inn med Microsoft", "auth.login.bitbucket": "Logg inn med Bitbucket", + "auth.login.clerk": "Logg inn med Clerk", + "auth.login.discord": "Logg inn med Discord", "auth.login.github": "Logg inn med GitHub", "auth.login.gitlab": "Logg inn med Gitlab", "auth.login.google": "Logg inn med Google", "auth.login.infomaniak": "Logg inn med Infomaniak", "auth.already_registered": "Allerede registrert?", "auth.confirm_password": "Bekreft passord", - "auth.forgot_password": "Glemt passord", + "auth.forgot_password_link": "Glemt passord?", + "auth.forgot_password_heading": "Gjenoppretting av passord", "auth.forgot_password_send_email": "Send e-post for tilbakestilling av passord", "auth.register_now": "Registrer deg", "auth.logout": "Logg ut", @@ -37,4 +40,4 @@ "resource.delete_configurations": "Slett alle konfigurasjonsfiler fra serveren permanent.", "database.delete_backups_locally": "Alle sikkerhetskopier vil bli slettet permanent fra lokal lagring.", "warning.sslipdomain": "Konfigurasjonen din er lagret, men sslip-domene med https er IKKE anbefalt, fordi Let's Encrypt-servere med dette offentlige domenet er hastighetsbegrenset (SSL-sertifikatvalidering vil mislykkes).

Bruk ditt eget domene i stedet." -} +} \ No newline at end of file diff --git a/lang/pl.json b/lang/pl.json new file mode 100644 index 000000000..bcd8e2393 --- /dev/null +++ b/lang/pl.json @@ -0,0 +1,44 @@ +{ + "auth.login": "Zaloguj", + "auth.login.authentik": "Zaloguj się przez Authentik", + "auth.login.azure": "Zaloguj się przez Microsoft", + "auth.login.bitbucket": "Zaloguj się przez Bitbucket", + "auth.login.clerk": "Zaloguj się przez Clerk", + "auth.login.discord": "Zaloguj się przez Discord", + "auth.login.github": "Zaloguj się przez GitHub", + "auth.login.gitlab": "Zaloguj się przez Gitlab", + "auth.login.google": "Zaloguj się przez Google", + "auth.login.infomaniak": "Zaloguj się przez Infomaniak", + "auth.login.zitadel": "Zaloguj się przez Zitadel", + "auth.already_registered": "Już zarejestrowany?", + "auth.confirm_password": "Potwierdź hasło", + "auth.forgot_password_link": "Zapomniałeś hasło?", + "auth.forgot_password_heading": "Odzyskiwanie hasła", + "auth.forgot_password_send_email": "Wyślij email resetujący hasło", + "auth.register_now": "Zarejestruj", + "auth.logout": "Wyloguj", + "auth.register": "Zarejestruj", + "auth.registration_disabled": "Rejestracja jest wyłączona. Skontaktuj się z administratorem.", + "auth.reset_password": "Zresetuj hasło", + "auth.failed": "Podane dane nie zgadzają się z naszymi rekordami.", + "auth.failed.callback": "Nie udało się przeprocesować callbacku od dostawcy logowania.", + "auth.failed.password": "Podane hasło jest nieprawidłowe.", + "auth.failed.email": "Nie znaleziono użytkownika z takim adresem email.", + "auth.throttle": "Zbyt wiele prób logowania. Spróbuj ponownie za :seconds sekund.", + "input.name": "Nazwa", + "input.email": "Email", + "input.password": "Hasło", + "input.password.again": "Hasło ponownie", + "input.code": "Jednorazowy kod", + "input.recovery_code": "Kod odzyskiwania", + "button.save": "Zapisz", + "repository.url": "Przykłady
Dla publicznych repozytoriów użyj https://....
Dla prywatnych repozytoriów, użyj git@....

https://github.com/coollabsio/coolify-examples - zostanie wybrany branch main
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify - zostanie wybrany branch nodejs-fastify
https://gitea.com/sedlav/expressjs.git - zostanie wybrany branch main
https://gitlab.com/andrasbacsai/nodejs-example.git - zostanie wybrany branch main", + "service.stop": "Ten serwis zostanie zatrzymany.", + "resource.docker_cleanup": "Uruchom Docker Cleanup (usunie nieużywane obrazy i cache buildera).", + "resource.non_persistent": "Wszystkie nietrwałe dane zostaną usunięte.", + "resource.delete_volumes": "Trwale usuń wszystkie wolumeny powiązane z tym zasobem.", + "resource.delete_connected_networks": "Trwale usuń wszystkie niepredefiniowane sieci powiązane z tym zasobem.", + "resource.delete_configurations": "Trwale usuń wszystkie pliki konfiguracyjne z serwera.", + "database.delete_backups_locally": "Wszystkie backupy zostaną trwale usunięte z lokalnej pamięci.", + "warning.sslipdomain": "Twoja konfiguracja została zapisana, lecz domena sslip z https jest NIEZALECANA, ponieważ serwery Let's Encrypt z tą publiczną domeną są pod rate limitem (walidacja certyfikatu SSL certificate się nie powiedzie).

Lepiej użyj własnej domeny." +} \ No newline at end of file diff --git a/lang/pt-br.json b/lang/pt-br.json index 2e793890f..f3ebb6c69 100644 --- a/lang/pt-br.json +++ b/lang/pt-br.json @@ -3,13 +3,16 @@ "auth.login.authentik": "Entrar com Authentik", "auth.login.azure": "Entrar com Microsoft", "auth.login.bitbucket": "Entrar com Bitbucket", + "auth.login.clerk": "Entrar com Clerk", + "auth.login.discord": "Entrar com Discord", "auth.login.github": "Entrar com GitHub", "auth.login.gitlab": "Entrar com Gitlab", "auth.login.google": "Entrar com Google", "auth.login.infomaniak": "Entrar com Infomaniak", "auth.already_registered": "Já tem uma conta?", "auth.confirm_password": "Confirmar senha", - "auth.forgot_password": "Esqueceu a senha", + "auth.forgot_password_link": "Esqueceu a senha?", + "auth.forgot_password_heading": "Recuperação de senha", "auth.forgot_password_send_email": "Enviar e-mail para redefinir senha", "auth.register_now": "Cadastre-se", "auth.logout": "Sair", @@ -37,4 +40,4 @@ "resource.delete_configurations": "Excluir permanentemente todos os arquivos de configuração do servidor.", "database.delete_backups_locally": "Todos os backups serão excluídos permanentemente do armazenamento local.", "warning.sslipdomain": "Sua configuração foi salva, mas o domínio sslip com https NÃO é recomendado, porque os servidores do Let's Encrypt com este domínio público têm limitação de taxa (a validação do certificado SSL falhará).

Use seu próprio domínio em vez disso." -} +} \ No newline at end of file diff --git a/lang/pt.json b/lang/pt.json index c5f393e65..08ad19df3 100644 --- a/lang/pt.json +++ b/lang/pt.json @@ -2,13 +2,16 @@ "auth.login": "Entrar", "auth.login.azure": "Entrar com Microsoft", "auth.login.bitbucket": "Entrar com Bitbucket", + "auth.login.clerk": "Entrar com Clerk", + "auth.login.discord": "Entrar com Discord", "auth.login.github": "Entrar com GitHub", "auth.login.gitlab": "Entrar com Gitlab", "auth.login.google": "Entrar com Google", "auth.login.infomaniak": "Entrar com Infomaniak", "auth.already_registered": "Já tem uma conta?", "auth.confirm_password": "Confirmar senha", - "auth.forgot_password": "Esqueceu a senha?", + "auth.forgot_password_link": "Esqueceu a senha?", + "auth.forgot_password_heading": "Recuperação de senha", "auth.forgot_password_send_email": "Enviar e-mail de redefinição de senha", "auth.register_now": "Cadastrar-se", "auth.logout": "Sair", @@ -28,4 +31,4 @@ "input.recovery_code": "Código de recuperação", "button.save": "Salvar", "repository.url": "Exemplos
Para repositórios públicos, use https://....
Para repositórios privados, use git@....

https://github.com/coollabsio/coolify-examples a branch main será selecionada
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify a branch nodejs-fastify será selecionada.
https://gitea.com/sedlav/expressjs.git a branch main será selecionada.
https://gitlab.com/andrasbacsai/nodejs-example.git a branch main será selecionada." -} +} \ No newline at end of file diff --git a/lang/ro.json b/lang/ro.json index 4c7968cfa..18028d087 100644 --- a/lang/ro.json +++ b/lang/ro.json @@ -2,13 +2,16 @@ "auth.login": "Autentificare", "auth.login.azure": "Autentificare prin Microsoft", "auth.login.bitbucket": "Autentificare prin Bitbucket", + "auth.login.clerk": "Autentificare prin Clerk", + "auth.login.discord": "Autentificare prin Discord", "auth.login.github": "Autentificare prin GitHub", "auth.login.gitlab": "Autentificare prin Gitlab", "auth.login.google": "Autentificare prin Google", "auth.login.infomaniak": "Autentificare prin Infomaniak", "auth.already_registered": "Sunteți deja înregistrat?", "auth.confirm_password": "Confirmați parola", - "auth.forgot_password": "Ați uitat parola", + "auth.forgot_password_link": "Ați uitat parola?", + "auth.forgot_password_heading": "Recuperare parolă", "auth.forgot_password_send_email": "Trimiteți e-mail-ul pentru resetarea parolei", "auth.register_now": "Înregistrare", "auth.logout": "Deconectare", @@ -35,4 +38,4 @@ "resource.delete_connected_networks": "Ștergeți definitiv toate rețelele non-predefinite asociate cu această resursă.", "resource.delete_configurations": "Ștergeți definitiv toate fișierele de configurare de pe server.", "database.delete_backups_locally": "Toate copiile de rezervă vor fi șterse definitiv din stocarea locală." -} +} \ No newline at end of file diff --git a/lang/tr.json b/lang/tr.json index 663c756f9..e3f34aa14 100644 --- a/lang/tr.json +++ b/lang/tr.json @@ -2,13 +2,16 @@ "auth.login": "Giriş", "auth.login.azure": "Microsoft ile Giriş Yap", "auth.login.bitbucket": "Bitbucket ile Giriş Yap", + "auth.login.clerk": "Clerk ile Giriş Yap", + "auth.login.discord": "Discord ile Giriş Yap", "auth.login.github": "GitHub ile Giriş Yap", "auth.login.gitlab": "GitLab ile Giriş Yap", "auth.login.google": "Google ile Giriş Yap", "auth.login.infomaniak": "Infomaniak ile Giriş Yap", "auth.already_registered": "Zaten kayıtlı mısınız?", "auth.confirm_password": "Şifreyi Onayla", - "auth.forgot_password": "Şifremi Unuttum", + "auth.forgot_password_link": "Şifrenizi mi unuttunuz?", + "auth.forgot_password_heading": "Şifre Kurtarma", "auth.forgot_password_send_email": "Şifre sıfırlama e-postası gönder", "auth.register_now": "Kayıt Ol", "auth.logout": "Çıkış Yap", @@ -36,4 +39,4 @@ "resource.delete_configurations": "Sunucudaki tüm yapılandırma dosyaları kalıcı olarak silinecek.", "database.delete_backups_locally": "Tüm yedekler yerel depolamadan kalıcı olarak silinecek.", "warning.sslipdomain": "Yapılandırmanız kaydedildi, ancak sslip domain ile https ÖNERİLMEZ, çünkü Let's Encrypt sunucuları bu genel domain ile sınırlandırılmıştır (SSL sertifikası doğrulaması başarısız olur).

Bunun yerine kendi domaininizi kullanın." -} +} \ No newline at end of file diff --git a/lang/vi.json b/lang/vi.json index bb43fd34d..76e380477 100644 --- a/lang/vi.json +++ b/lang/vi.json @@ -2,13 +2,16 @@ "auth.login": "Đăng Nhập", "auth.login.azure": "Đăng Nhập Bằng Microsoft", "auth.login.bitbucket": "Đăng Nhập Bằng Bitbucket", + "auth.login.clerk": "Đăng Nhập Bằng Clerk", + "auth.login.discord": "Đăng Nhập Bằng Discord", "auth.login.github": "Đăng Nhập Bằng GitHub", "auth.login.gitlab": "Đăng Nhập Bằng Gitlab", "auth.login.google": "Đăng Nhập Bằng Google", "auth.login.infomaniak": "Đăng Nhập Bằng Infomaniak", "auth.already_registered": "Đã đăng ký?", "auth.confirm_password": "Nhập lại mật khẩu", - "auth.forgot_password": "Quên mật khẩu", + "auth.forgot_password_link": "Quên mật khẩu?", + "auth.forgot_password_heading": "Khôi phục mật khẩu", "auth.forgot_password_send_email": "Gửi email đặt lại mật khẩu", "auth.register_now": "Đăng ký ngay", "auth.logout": "Đăng xuất", @@ -28,4 +31,4 @@ "input.recovery_code": "Mã khôi phục", "button.save": "Lưu", "repository.url": "Ví dụ
Với repo công khai, sử dụng https://....
Với repo riêng tư, sử dụng git@....

https://github.com/coollabsio/coolify-examples nhánh chính sẽ được chọn
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nhánh nodejs-fastify sẽ được chọn.
https://gitea.com/sedlav/expressjs.git nhánh chính sẽ được chọn.
https://gitlab.com/andrasbacsai/nodejs-example.git nhánh chính sẽ được chọn." -} +} \ No newline at end of file diff --git a/lang/zh-cn.json b/lang/zh-cn.json index 944887a5f..530621ee1 100644 --- a/lang/zh-cn.json +++ b/lang/zh-cn.json @@ -2,13 +2,16 @@ "auth.login": "登录", "auth.login.azure": "使用 Microsoft 登录", "auth.login.bitbucket": "使用 Bitbucket 登录", + "auth.login.clerk": "使用 Clerk 登录", + "auth.login.discord": "使用 Discord 登录", "auth.login.github": "使用 GitHub 登录", "auth.login.gitlab": "使用 Gitlab 登录", "auth.login.google": "使用 Google 登录", "auth.login.infomaniak": "使用 Infomaniak 登录", "auth.already_registered": "已经注册?", "auth.confirm_password": "确认密码", - "auth.forgot_password": "忘记密码", + "auth.forgot_password_link": "忘记密码?", + "auth.forgot_password_heading": "密码找回", "auth.forgot_password_send_email": "发送密码重置邮件", "auth.register_now": "注册", "auth.logout": "退出登录", @@ -28,4 +31,4 @@ "input.recovery_code": "恢复码", "button.save": "保存", "repository.url": "示例
对于公共代码仓库,请使用 https://...
对于私有代码仓库,请使用 git@...

https://github.com/coollabsio/coolify-examples main 分支将被选择
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify 分支将被选择。
https://gitea.com/sedlav/expressjs.git main 分支将被选择。
https://gitlab.com/andrasbacsai/nodejs-example.git main 分支将被选择" -} +} \ No newline at end of file diff --git a/lang/zh-tw.json b/lang/zh-tw.json index c42ebb33e..aa078104b 100644 --- a/lang/zh-tw.json +++ b/lang/zh-tw.json @@ -2,13 +2,16 @@ "auth.login": "登入", "auth.login.azure": "使用 Microsoft 登入", "auth.login.bitbucket": "使用 Bitbucket 登入", + "auth.login.clerk": "使用 Clerk 登入", + "auth.login.discord": "使用 Discord 登入", "auth.login.github": "使用 GitHub 登入", "auth.login.gitlab": "使用 Gitlab 登入", "auth.login.google": "使用 Google 登入", "auth.login.infomaniak": "使用 Infomaniak 登入", "auth.already_registered": "已經註冊?", "auth.confirm_password": "確認密碼", - "auth.forgot_password": "忘記密碼", + "auth.forgot_password_link": "忘記密碼?", + "auth.forgot_password_heading": "密碼找回", "auth.forgot_password_send_email": "發送重設密碼電郵", "auth.register_now": "註冊", "auth.logout": "登出", @@ -28,4 +31,4 @@ "input.recovery_code": "恢復碼", "button.save": "儲存", "repository.url": "例子
對於公共代碼倉庫,請使用 https://...
對於私有代碼倉庫,請使用 git@...

https://github.com/coollabsio/coolify-examples main 分支將被選擇
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify 分支將被選擇。
https://gitea.com/sedlav/expressjs.git main 分支將被選擇。
https://gitlab.com/andrasbacsai/nodejs-example.git main 分支將被選擇。" -} +} \ No newline at end of file diff --git a/other/nightly/install.sh b/other/nightly/install.sh index e9f54952a..92ad12302 100755 --- a/other/nightly/install.sh +++ b/other/nightly/install.sh @@ -253,6 +253,11 @@ if [ "$OS_TYPE" = "endeavouros" ]; then OS_TYPE="arch" fi +# Check if the OS is Cachy OS, if so, change it to arch +if [ "$OS_TYPE" = "cachyos" ]; then + OS_TYPE="arch" +fi + # Check if the OS is Asahi Linux, if so, change it to fedora if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then OS_TYPE="fedora" @@ -844,7 +849,7 @@ IPV6_PUBLIC_IP=$(curl -6s https://ifconfig.io || true) echo -e "\nYour instance is ready to use!\n" if [ -n "$IPV4_PUBLIC_IP" ]; then - echo -e "You can access Coolify through your Public IPV4: http://$(curl -4s https://ifconfig.io):8000" + echo -e "You can access Coolify through your Public IPV4: http://$IPV4_PUBLIC_IP:8000" fi if [ -n "$IPV6_PUBLIC_IP" ]; then echo -e "You can access Coolify through your Public IPv6: http://[$IPV6_PUBLIC_IP]:8000" diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 9fde1e53b..8d362115e 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.420" + "version": "4.0.0-beta.420.2" }, "nightly": { - "version": "4.0.0-beta.421" + "version": "4.0.0-beta.420.3" }, "helper": { "version": "1.0.8" diff --git a/package-lock.json b/package-lock.json index d86caea87..34b2c1dd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,13 +74,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", - "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -90,9 +90,9 @@ } }, "node_modules/@babel/types": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", - "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -104,9 +104,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", "cpu": [ "ppc64" ], @@ -121,9 +121,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", "cpu": [ "arm" ], @@ -138,9 +138,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", "cpu": [ "arm64" ], @@ -155,9 +155,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", "cpu": [ "x64" ], @@ -172,9 +172,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", "cpu": [ "arm64" ], @@ -189,9 +189,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", "cpu": [ "x64" ], @@ -206,9 +206,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", "cpu": [ "arm64" ], @@ -223,9 +223,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", "cpu": [ "x64" ], @@ -240,9 +240,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", "cpu": [ "arm" ], @@ -257,9 +257,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", "cpu": [ "arm64" ], @@ -274,9 +274,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", "cpu": [ "ia32" ], @@ -291,9 +291,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", "cpu": [ "loong64" ], @@ -308,9 +308,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", "cpu": [ "mips64el" ], @@ -325,9 +325,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", "cpu": [ "ppc64" ], @@ -342,9 +342,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", "cpu": [ "riscv64" ], @@ -359,9 +359,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", "cpu": [ "s390x" ], @@ -376,9 +376,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", "cpu": [ "x64" ], @@ -393,9 +393,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", "cpu": [ "arm64" ], @@ -410,9 +410,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", "cpu": [ "x64" ], @@ -427,9 +427,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", "cpu": [ "arm64" ], @@ -444,9 +444,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", "cpu": [ "x64" ], @@ -460,10 +460,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", "cpu": [ "x64" ], @@ -478,9 +495,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", "cpu": [ "arm64" ], @@ -495,9 +512,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", "cpu": [ "ia32" ], @@ -512,9 +529,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", "cpu": [ "x64" ], @@ -529,9 +546,9 @@ } }, "node_modules/@ioredis/commands": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.0.tgz", + "integrity": "sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==", "license": "MIT" }, "node_modules/@isaacs/fs-minipass": { @@ -548,18 +565,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -572,27 +585,17 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -601,9 +604,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", - "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", + "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", "cpu": [ "arm" ], @@ -615,9 +618,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", - "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", + "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", "cpu": [ "arm64" ], @@ -629,9 +632,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", - "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", + "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", "cpu": [ "arm64" ], @@ -643,9 +646,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", - "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", + "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", "cpu": [ "x64" ], @@ -657,9 +660,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", - "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", + "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", "cpu": [ "arm64" ], @@ -671,9 +674,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", - "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", + "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", "cpu": [ "x64" ], @@ -685,9 +688,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", - "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", + "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", "cpu": [ "arm" ], @@ -699,9 +702,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", - "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", + "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", "cpu": [ "arm" ], @@ -713,9 +716,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", - "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", + "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", "cpu": [ "arm64" ], @@ -727,9 +730,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", - "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", + "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", "cpu": [ "arm64" ], @@ -741,9 +744,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", - "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", + "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", "cpu": [ "loong64" ], @@ -754,10 +757,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", - "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", + "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", "cpu": [ "ppc64" ], @@ -769,9 +772,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", - "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", + "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", "cpu": [ "riscv64" ], @@ -783,9 +786,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", - "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", + "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", "cpu": [ "riscv64" ], @@ -797,9 +800,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", - "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", + "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", "cpu": [ "s390x" ], @@ -811,9 +814,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", - "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", "cpu": [ "x64" ], @@ -825,9 +828,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", - "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", + "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", "cpu": [ "x64" ], @@ -839,9 +842,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", - "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", + "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", "cpu": [ "arm64" ], @@ -853,9 +856,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", - "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", + "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", "cpu": [ "ia32" ], @@ -867,9 +870,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", - "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", + "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", "cpu": [ "x64" ], @@ -1192,9 +1195,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -1544,9 +1547,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1620,9 +1623,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", - "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1633,31 +1636,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.4", - "@esbuild/android-arm": "0.25.4", - "@esbuild/android-arm64": "0.25.4", - "@esbuild/android-x64": "0.25.4", - "@esbuild/darwin-arm64": "0.25.4", - "@esbuild/darwin-x64": "0.25.4", - "@esbuild/freebsd-arm64": "0.25.4", - "@esbuild/freebsd-x64": "0.25.4", - "@esbuild/linux-arm": "0.25.4", - "@esbuild/linux-arm64": "0.25.4", - "@esbuild/linux-ia32": "0.25.4", - "@esbuild/linux-loong64": "0.25.4", - "@esbuild/linux-mips64el": "0.25.4", - "@esbuild/linux-ppc64": "0.25.4", - "@esbuild/linux-riscv64": "0.25.4", - "@esbuild/linux-s390x": "0.25.4", - "@esbuild/linux-x64": "0.25.4", - "@esbuild/netbsd-arm64": "0.25.4", - "@esbuild/netbsd-x64": "0.25.4", - "@esbuild/openbsd-arm64": "0.25.4", - "@esbuild/openbsd-x64": "0.25.4", - "@esbuild/sunos-x64": "0.25.4", - "@esbuild/win32-arm64": "0.25.4", - "@esbuild/win32-ia32": "0.25.4", - "@esbuild/win32-x64": "0.25.4" + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" } }, "node_modules/estree-walker": { @@ -1668,9 +1672,9 @@ "license": "MIT" }, "node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1683,9 +1687,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "dev": true, "funding": [ { @@ -1704,15 +1708,16 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -1870,9 +1875,9 @@ } }, "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", "dev": true, "license": "MIT", "bin": { @@ -2306,9 +2311,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -2392,9 +2397,9 @@ } }, "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "dev": true, "license": "MIT", "peer": true, @@ -2424,13 +2429,13 @@ } }, "node_modules/rollup": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", - "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", + "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -2440,26 +2445,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.2", - "@rollup/rollup-android-arm64": "4.40.2", - "@rollup/rollup-darwin-arm64": "4.40.2", - "@rollup/rollup-darwin-x64": "4.40.2", - "@rollup/rollup-freebsd-arm64": "4.40.2", - "@rollup/rollup-freebsd-x64": "4.40.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", - "@rollup/rollup-linux-arm-musleabihf": "4.40.2", - "@rollup/rollup-linux-arm64-gnu": "4.40.2", - "@rollup/rollup-linux-arm64-musl": "4.40.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-musl": "4.40.2", - "@rollup/rollup-linux-s390x-gnu": "4.40.2", - "@rollup/rollup-linux-x64-gnu": "4.40.2", - "@rollup/rollup-linux-x64-musl": "4.40.2", - "@rollup/rollup-win32-arm64-msvc": "4.40.2", - "@rollup/rollup-win32-ia32-msvc": "4.40.2", - "@rollup/rollup-win32-x64-msvc": "4.40.2", + "@rollup/rollup-android-arm-eabi": "4.46.2", + "@rollup/rollup-android-arm64": "4.46.2", + "@rollup/rollup-darwin-arm64": "4.46.2", + "@rollup/rollup-darwin-x64": "4.46.2", + "@rollup/rollup-freebsd-arm64": "4.46.2", + "@rollup/rollup-freebsd-x64": "4.46.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", + "@rollup/rollup-linux-arm-musleabihf": "4.46.2", + "@rollup/rollup-linux-arm64-gnu": "4.46.2", + "@rollup/rollup-linux-arm64-musl": "4.46.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", + "@rollup/rollup-linux-ppc64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-musl": "4.46.2", + "@rollup/rollup-linux-s390x-gnu": "4.46.2", + "@rollup/rollup-linux-x64-gnu": "4.46.2", + "@rollup/rollup-linux-x64-musl": "4.46.2", + "@rollup/rollup-win32-arm64-msvc": "4.46.2", + "@rollup/rollup-win32-ia32-msvc": "4.46.2", + "@rollup/rollup-win32-x64-msvc": "4.46.2", "fsevents": "~2.3.2" } }, @@ -2600,9 +2605,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/public/heart.png b/public/heart.png new file mode 100644 index 000000000..c44a21301 Binary files /dev/null and b/public/heart.png differ diff --git a/public/svgs/chroma.svg b/public/svgs/chroma.svg new file mode 100644 index 000000000..930288fbf --- /dev/null +++ b/public/svgs/chroma.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/public/svgs/excalidraw.svg b/public/svgs/excalidraw.svg new file mode 100644 index 000000000..ee996762e --- /dev/null +++ b/public/svgs/excalidraw.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/svgs/github-runner.png b/public/svgs/github-runner.png new file mode 100644 index 000000000..fb5b5c1b7 Binary files /dev/null and b/public/svgs/github-runner.png differ diff --git a/public/svgs/gowa.svg b/public/svgs/gowa.svg new file mode 100644 index 000000000..1121b05bc --- /dev/null +++ b/public/svgs/gowa.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/homebox.svg b/public/svgs/homebox.svg new file mode 100644 index 000000000..08670bbb9 --- /dev/null +++ b/public/svgs/homebox.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/svgs/hoarder.svg b/public/svgs/karakeep.svg similarity index 100% rename from public/svgs/hoarder.svg rename to public/svgs/karakeep.svg diff --git a/public/svgs/langfuse.png b/public/svgs/langfuse.png deleted file mode 100644 index 8dec0fe4a..000000000 Binary files a/public/svgs/langfuse.png and /dev/null differ diff --git a/public/svgs/langfuse.svg b/public/svgs/langfuse.svg new file mode 100644 index 000000000..b04e07490 --- /dev/null +++ b/public/svgs/langfuse.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/svgs/librechat.svg b/public/svgs/librechat.svg new file mode 100644 index 000000000..36a536d65 --- /dev/null +++ b/public/svgs/librechat.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/svgs/matrix.svg b/public/svgs/matrix.svg new file mode 100644 index 000000000..bc41720a2 --- /dev/null +++ b/public/svgs/matrix.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/public/svgs/miniflux.svg b/public/svgs/miniflux.svg new file mode 100644 index 000000000..33ae73a87 --- /dev/null +++ b/public/svgs/miniflux.svg @@ -0,0 +1 @@ +icon \ No newline at end of file diff --git a/public/svgs/openpanel.svg b/public/svgs/openpanel.svg new file mode 100644 index 000000000..8508fc69e --- /dev/null +++ b/public/svgs/openpanel.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/pihole.svg b/public/svgs/pihole.svg new file mode 100644 index 000000000..a4efefcc8 --- /dev/null +++ b/public/svgs/pihole.svg @@ -0,0 +1 @@ +NewVortex \ No newline at end of file diff --git a/public/svgs/pingvinshare.svg b/public/svgs/pingvinshare.svg new file mode 100644 index 000000000..4f1f7a7bc --- /dev/null +++ b/public/svgs/pingvinshare.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/sequin.svg b/public/svgs/sequin.svg new file mode 100644 index 000000000..623bc1159 --- /dev/null +++ b/public/svgs/sequin.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/svgs/triliumnext.svg b/public/svgs/triliumnext.svg new file mode 100644 index 000000000..173712891 --- /dev/null +++ b/public/svgs/triliumnext.svg @@ -0,0 +1,28 @@ + + + TriliumNext Notes + + + + + + + + + + + + + + + diff --git a/resources/css/utilities.css b/resources/css/utilities.css index 4993b2302..d09d7f49c 100644 --- a/resources/css/utilities.css +++ b/resources/css/utilities.css @@ -42,7 +42,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-none disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300; + @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; } @utility alert-success { @@ -122,7 +122,7 @@ @utility custom-modal { } @utility navbar-main { - @apply flex flex-col gap-4 justify-items-start pb-2 border-b-2 border-solid h-fit md:flex-row sm:justify-between dark:border-coolgray-200 md:items-center; + @apply flex flex-col gap-4 justify-items-start pb-2 border-b-2 border-solid h-fit md:flex-row sm:justify-between dark:border-coolgray-200 border-neutral-200 md:items-center; } @utility loading { diff --git a/resources/js/terminal.js b/resources/js/terminal.js index 10535f3ea..b49aad9cf 100644 --- a/resources/js/terminal.js +++ b/resources/js/terminal.js @@ -48,6 +48,18 @@ export function initializeTerminalComponent() { this.sendCommandWhenReady({ command: command }); }); + this.$wire.on('terminal-should-focus', () => { + // Wait for terminal to be ready, then focus + const focusWhenReady = () => { + if (this.terminalActive && this.term) { + this.term.focus(); + } else { + setTimeout(focusWhenReady, 100); + } + }; + focusWhenReady(); + }); + this.keepAliveInterval = setInterval(this.keepAlive.bind(this), 30000); this.$watch('terminalActive', (active) => { @@ -353,6 +365,15 @@ export function initializeTerminalComponent() { this.resizeTerminal(); }, 200); + // Ensure terminal gets focus after connection with multiple attempts + setTimeout(() => { + this.term.focus(); + }, 100); + + setTimeout(() => { + this.term.focus(); + }, 500); + // Notify parent component that terminal is connected this.$wire.dispatch('terminalConnected'); } else if (event.data === 'unprocessable') { diff --git a/resources/views/auth/forgot-password.blade.php b/resources/views/auth/forgot-password.blade.php index 249aa18f9..66a924fb8 100644 --- a/resources/views/auth/forgot-password.blade.php +++ b/resources/views/auth/forgot-password.blade.php @@ -4,7 +4,7 @@ Coolify
- {{ __('auth.forgot_password') }} + {{ __('auth.forgot_password_heading') }}
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 42faf517f..8bd8e81fc 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -23,7 +23,7 @@ required label="{{ __('input.password') }}" /> - {{ __('auth.forgot_password') }}? + {{ __('auth.forgot_password_link') }} @else - {{ __('auth.forgot_password') }}? + {{ __('auth.forgot_password_link') }} @endenv diff --git a/resources/views/components/loading-on-button.blade.php b/resources/views/components/loading-on-button.blade.php index 78a1f9726..a451023aa 100644 --- a/resources/views/components/loading-on-button.blade.php +++ b/resources/views/components/loading-on-button.blade.php @@ -3,7 +3,7 @@ @if (isset($text))
{{ $text }}
@endif - - @@ -362,7 +362,7 @@ class="{{ request()->is('settings*') ? 'menu-item-active menu-item' : 'menu-item @endpersist @endif -
  • + {{--
  • @@ -372,7 +372,7 @@ class="{{ request()->is('onboarding*') ? 'menu-item-active menu-item' : 'menu-it Onboarding -
  • + --}}
  • @@ -410,7 +410,7 @@ class="{{ request()->is('onboarding*') ? 'menu-item-active menu-item' : 'menu-it
    @csrf -
  • + + @endisset + diff --git a/resources/views/components/settings/navbar.blade.php b/resources/views/components/settings/navbar.blade.php index a54dc1886..395c0953e 100644 --- a/resources/views/components/settings/navbar.blade.php +++ b/resources/views/components/settings/navbar.blade.php @@ -5,19 +5,19 @@ diff --git a/resources/views/components/settings/sidebar.blade.php b/resources/views/components/settings/sidebar.blade.php new file mode 100644 index 000000000..ef2f0d5d2 --- /dev/null +++ b/resources/views/components/settings/sidebar.blade.php @@ -0,0 +1,8 @@ +
    + General + Advanced + Updates +
    diff --git a/resources/views/components/status/index.blade.php b/resources/views/components/status/index.blade.php index fe9569c8a..65beace65 100644 --- a/resources/views/components/status/index.blade.php +++ b/resources/views/components/status/index.blade.php @@ -13,11 +13,18 @@ @endif @if (!str($resource->status)->contains('exited') && $showRefreshButton) - + @endif diff --git a/resources/views/components/status/services.blade.php b/resources/views/components/status/services.blade.php index 5a52335a7..677cc6f45 100644 --- a/resources/views/components/status/services.blade.php +++ b/resources/views/components/status/services.blade.php @@ -1,5 +1,7 @@ @if (str($complexStatus)->contains('running')) +@elseif(str($complexStatus)->contains('starting')) + @elseif(str($complexStatus)->contains('restarting')) @elseif(str($complexStatus)->contains('degraded')) @@ -8,11 +10,18 @@ @endif @if (!str($complexStatus)->contains('exited') && $showRefreshButton) - + @endif diff --git a/resources/views/components/team/navbar.blade.php b/resources/views/components/team/navbar.blade.php index aa88aad51..89bdf8654 100644 --- a/resources/views/components/team/navbar.blade.php +++ b/resources/views/components/team/navbar.blade.php @@ -9,16 +9,16 @@