feat(deployment): add pull request filtering and pagination to deployment and backup execution components
fix(ui): make them more stylish yeah
This commit is contained in:
		
							
								
								
									
										87
									
								
								CLAUDE.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								CLAUDE.md
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
			
		||||
@@ -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 @@ class Index extends Component
 | 
			
		||||
        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()
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ namespace App\Livewire\Project\Database;
 | 
			
		||||
 | 
			
		||||
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 @@ class BackupExecutions extends Component
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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 @@ class BackupExecutions extends Component
 | 
			
		||||
 | 
			
		||||
    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()
 | 
			
		||||
 
 | 
			
		||||
@@ -836,9 +836,14 @@ class Application extends BaseModel
 | 
			
		||||
        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();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,18 @@ class ScheduledDatabaseBackup extends BaseModel
 | 
			
		||||
        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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,31 +3,36 @@
 | 
			
		||||
    <h1>Deployments</h1>
 | 
			
		||||
    <livewire:project.shared.configuration-checker :resource="$application" />
 | 
			
		||||
    <livewire:project.application.heading :application="$application" />
 | 
			
		||||
    <div class="flex flex-col gap-2 pb-10"
 | 
			
		||||
        @if (!$skip) wire:poll.5000ms='reload_deployments' @endif>
 | 
			
		||||
    <div class="flex flex-col gap-2 pb-10" @if (!$skip) wire:poll.5000ms='reloadDeployments' @endif>
 | 
			
		||||
        <div class="flex items-end gap-2">
 | 
			
		||||
            <h2>Deployments <span class="text-xs">({{ $deployments_count }})</span></h2>
 | 
			
		||||
            @if ($deployments_count > 0)
 | 
			
		||||
                <x-forms.button disabled="{{ !$show_prev }}" wire:click="previous_page('{{ $default_take }}')">
 | 
			
		||||
                    <svg class="w-4 h-4" viewBox="0 0 24 24">
 | 
			
		||||
                        <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
 | 
			
		||||
                            stroke-width="2" d="m14 6l-6 6l6 6z" />
 | 
			
		||||
                    </svg>
 | 
			
		||||
                </x-forms.button>
 | 
			
		||||
                <x-forms.button disabled="{{ !$show_next }}" wire:click="next_page('{{ $default_take }}')">
 | 
			
		||||
                    <svg class="w-4 h-4" viewBox="0 0 24 24">
 | 
			
		||||
                        <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
 | 
			
		||||
                            stroke-width="2" d="m10 18l6-6l-6-6z" />
 | 
			
		||||
                    </svg>
 | 
			
		||||
                </x-forms.button>
 | 
			
		||||
                <div class="flex items-center gap-2">
 | 
			
		||||
                    <x-forms.button disabled="{{ !$showPrev }}" wire:click="previousPage('{{ $defaultTake }}')">
 | 
			
		||||
                        <svg class="w-4 h-4" viewBox="0 0 24 24">
 | 
			
		||||
                            <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
 | 
			
		||||
                                stroke-width="2" d="m14 6l-6 6l6 6z" />
 | 
			
		||||
                        </svg>
 | 
			
		||||
                    </x-forms.button>
 | 
			
		||||
                    <span class="text-sm text-gray-600 dark:text-gray-400 px-2">
 | 
			
		||||
                        Page {{ $currentPage }} of {{ ceil($deployments_count / $defaultTake) }}
 | 
			
		||||
                    </span>
 | 
			
		||||
                    <x-forms.button disabled="{{ !$showNext }}" wire:click="nextPage('{{ $defaultTake }}')">
 | 
			
		||||
                        <svg class="w-4 h-4" viewBox="0 0 24 24">
 | 
			
		||||
                            <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
 | 
			
		||||
                                stroke-width="2" d="m10 18l6-6l-6-6z" />
 | 
			
		||||
                        </svg>
 | 
			
		||||
                    </x-forms.button>
 | 
			
		||||
                </div>
 | 
			
		||||
            @endif
 | 
			
		||||
        </div>
 | 
			
		||||
        @if ($deployments_count > 0)
 | 
			
		||||
            <form class="flex items-end gap-2">
 | 
			
		||||
                <x-forms.input id="pull_request_id" label="Pull Request"></x-forms.input>
 | 
			
		||||
                <x-forms.button type="submit">Filter</x-forms.button>
 | 
			
		||||
            </form>
 | 
			
		||||
        @endif
 | 
			
		||||
        <form class="flex items-end gap-2">
 | 
			
		||||
            <x-forms.input id="pull_request_id" type="number" min="1" label="Pull Request Id"></x-forms.input>
 | 
			
		||||
            <x-forms.button type="submit">Filter</x-forms.button>
 | 
			
		||||
            @if ($pull_request_id)
 | 
			
		||||
                <x-forms.button type="button" wire:click="clearFilter">Clear</x-forms.button>
 | 
			
		||||
            @endif
 | 
			
		||||
        </form>
 | 
			
		||||
        @forelse ($deployments as $deployment)
 | 
			
		||||
            <div @class([
 | 
			
		||||
                'p-2 border-l-2 bg-white dark:bg-coolgray-100',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,36 @@
 | 
			
		||||
<div wire:init='refreshBackupExecutions'>
 | 
			
		||||
    @isset($backup)
 | 
			
		||||
        <div class="flex items-center gap-2">
 | 
			
		||||
            <h3 class="py-4">Executions</h3>
 | 
			
		||||
            <h3 class="py-4">Executions <span class="text-xs">({{ $executions_count }})</span></h3>
 | 
			
		||||
            @if ($executions_count > 0)
 | 
			
		||||
                <div class="flex items-center gap-2">
 | 
			
		||||
                    <x-forms.button disabled="{{ !$showPrev }}" wire:click="previousPage('{{ $defaultTake }}')">
 | 
			
		||||
                        <svg class="w-4 h-4" viewBox="0 0 24 24">
 | 
			
		||||
                            <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
 | 
			
		||||
                                stroke-width="2" d="m14 6l-6 6l6 6z" />
 | 
			
		||||
                        </svg>
 | 
			
		||||
                    </x-forms.button>
 | 
			
		||||
                    <span class="text-sm text-gray-600 dark:text-gray-400 px-2">
 | 
			
		||||
                        Page {{ $currentPage }} of {{ ceil($executions_count / $defaultTake) }}
 | 
			
		||||
                    </span>
 | 
			
		||||
                    <x-forms.button disabled="{{ !$showNext }}" wire:click="nextPage('{{ $defaultTake }}')">
 | 
			
		||||
                        <svg class="w-4 h-4" viewBox="0 0 24 24">
 | 
			
		||||
                            <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
 | 
			
		||||
                                stroke-width="2" d="m10 18l6-6l-6-6z" />
 | 
			
		||||
                        </svg>
 | 
			
		||||
                    </x-forms.button>
 | 
			
		||||
                </div>
 | 
			
		||||
            @endif
 | 
			
		||||
            <x-forms.button wire:click='cleanupFailed'>Cleanup Failed Backups</x-forms.button>
 | 
			
		||||
            <x-modal-confirmation title="Cleanup Deleted Backup Entries?" buttonTitle="Cleanup Deleted" isErrorButton
 | 
			
		||||
                submitAction="cleanupDeleted()" 
 | 
			
		||||
                :actions="['This will permanently delete all backup execution entries that are marked as deleted from local storage.', 'This only removes database entries, not actual backup files.']" 
 | 
			
		||||
                confirmationText="cleanup deleted backups"
 | 
			
		||||
                confirmationLabel="Please confirm by typing 'cleanup deleted backups' below"
 | 
			
		||||
                shortConfirmationLabel="Confirmation" />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div wire:poll.5000ms="refreshBackupExecutions" class="flex flex-col gap-4">
 | 
			
		||||
        <div @if (!$skip) wire:poll.5000ms="refreshBackupExecutions" @endif
 | 
			
		||||
            class="flex flex-col gap-4">
 | 
			
		||||
            @forelse($executions as $execution)
 | 
			
		||||
                <div wire:key="{{ data_get($execution, 'id') }}" @class([
 | 
			
		||||
                    'flex flex-col border-l-2 transition-colors p-4 bg-white dark:bg-coolgray-100 text-black dark:text-white',
 | 
			
		||||
 
 | 
			
		||||
@@ -20,26 +20,183 @@
 | 
			
		||||
        @else
 | 
			
		||||
            @forelse($database->scheduledBackups as $backup)
 | 
			
		||||
                @if ($type == 'database')
 | 
			
		||||
                    <a class="box"
 | 
			
		||||
                    <a @class([
 | 
			
		||||
                        'flex flex-col border-l-2 transition-colors p-4 cursor-pointer bg-white hover:bg-gray-100 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 text-black dark:text-white',
 | 
			
		||||
                        'border-blue-500/50 border-dashed' =>
 | 
			
		||||
                            $backup->latest_log &&
 | 
			
		||||
                            data_get($backup->latest_log, 'status') === 'running',
 | 
			
		||||
                        'border-error' =>
 | 
			
		||||
                            $backup->latest_log &&
 | 
			
		||||
                            data_get($backup->latest_log, 'status') === 'failed',
 | 
			
		||||
                        'border-success' =>
 | 
			
		||||
                            $backup->latest_log &&
 | 
			
		||||
                            data_get($backup->latest_log, 'status') === 'success',
 | 
			
		||||
                        'border-gray-200 dark:border-coolgray-300' => !$backup->latest_log,
 | 
			
		||||
                    ])
 | 
			
		||||
                        href="{{ route('project.database.backup.execution', [...$parameters, 'backup_uuid' => $backup->uuid]) }}">
 | 
			
		||||
                        <div class="flex flex-col">
 | 
			
		||||
                            <div>Frequency: {{ $backup->frequency }}
 | 
			
		||||
                                ({{ data_get($backup->server(), 'settings.server_timezone', 'Instance timezone') }})
 | 
			
		||||
                        @if ($backup->latest_log && data_get($backup->latest_log, 'status') === 'running')
 | 
			
		||||
                            <div class="absolute top-2 right-2">
 | 
			
		||||
                                <x-loading />
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div>
 | 
			
		||||
                        @endif
 | 
			
		||||
                        <div class="flex items-center gap-2 mb-2">
 | 
			
		||||
                            @if ($backup->latest_log)
 | 
			
		||||
                                <span @class([
 | 
			
		||||
                                    'px-3 py-1 rounded-md text-xs font-medium tracking-wide shadow-xs',
 | 
			
		||||
                                    'bg-blue-100/80 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300 dark:shadow-blue-900/5' =>
 | 
			
		||||
                                        data_get($backup->latest_log, 'status') === 'running',
 | 
			
		||||
                                    'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200 dark:shadow-red-900/5' =>
 | 
			
		||||
                                        data_get($backup->latest_log, 'status') === 'failed',
 | 
			
		||||
                                    'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200 dark:shadow-green-900/5' =>
 | 
			
		||||
                                        data_get($backup->latest_log, 'status') === 'success',
 | 
			
		||||
                                ])>
 | 
			
		||||
                                    @php
 | 
			
		||||
                                        $statusText = match (data_get($backup->latest_log, 'status')) {
 | 
			
		||||
                                            'success' => 'Success',
 | 
			
		||||
                                            'running' => 'In Progress',
 | 
			
		||||
                                            'failed' => 'Failed',
 | 
			
		||||
                                            default => ucfirst(data_get($backup->latest_log, 'status')),
 | 
			
		||||
                                        };
 | 
			
		||||
                                    @endphp
 | 
			
		||||
                                    {{ $statusText }}
 | 
			
		||||
                                </span>
 | 
			
		||||
                            @else
 | 
			
		||||
                                <span
 | 
			
		||||
                                    class="px-3 py-1 rounded-md text-xs font-medium tracking-wide shadow-xs bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-200">
 | 
			
		||||
                                    No executions yet
 | 
			
		||||
                                </span>
 | 
			
		||||
                            @endif
 | 
			
		||||
                            <h3 class="font-semibold">{{ $backup->frequency }}</h3>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="text-gray-600 dark:text-gray-400 text-sm">
 | 
			
		||||
                            @if ($backup->latest_log)
 | 
			
		||||
                                Started:
 | 
			
		||||
                                {{ formatDateInServerTimezone(data_get($backup->latest_log, 'created_at'), $backup->server()) }}
 | 
			
		||||
                                @if (data_get($backup->latest_log, 'status') !== 'running')
 | 
			
		||||
                                    <br>Ended:
 | 
			
		||||
                                    {{ formatDateInServerTimezone(data_get($backup->latest_log, 'finished_at'), $backup->server()) }}
 | 
			
		||||
                                    <br>Duration:
 | 
			
		||||
                                    {{ calculateDuration(data_get($backup->latest_log, 'created_at'), data_get($backup->latest_log, 'finished_at')) }}
 | 
			
		||||
                                    <br>Finished
 | 
			
		||||
                                    {{ \Carbon\Carbon::parse(data_get($backup->latest_log, 'finished_at'))->diffForHumans() }}
 | 
			
		||||
                                @endif
 | 
			
		||||
                                @if ($backup->save_s3)
 | 
			
		||||
                                    <br>S3 Storage: Enabled
 | 
			
		||||
                                @endif
 | 
			
		||||
                                @if (data_get($backup->latest_log, 'status') === 'success')
 | 
			
		||||
                                    @php
 | 
			
		||||
                                        $size = data_get($backup->latest_log, 'size', 0);
 | 
			
		||||
                                        $sizeFormatted =
 | 
			
		||||
                                            $size > 0 ? number_format($size / 1024 / 1024, 2) . ' MB' : 'Unknown';
 | 
			
		||||
                                    @endphp
 | 
			
		||||
                                    <br>Last Backup Size: {{ $sizeFormatted }}
 | 
			
		||||
                                @endif
 | 
			
		||||
                            @else
 | 
			
		||||
                                Last Run: Never
 | 
			
		||||
                                <br>Total Executions: 0
 | 
			
		||||
                                @if ($backup->save_s3)
 | 
			
		||||
                                    <br>S3 Storage: Enabled
 | 
			
		||||
                                @endif
 | 
			
		||||
                            @endif
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </a>
 | 
			
		||||
                @else
 | 
			
		||||
                    <div class="box" wire:click="setSelectedBackup('{{ data_get($backup, 'id') }}')">
 | 
			
		||||
                        <div @class([
 | 
			
		||||
                            'border-coollabs' =>
 | 
			
		||||
                                data_get($backup, 'id') === data_get($selectedBackup, 'id'),
 | 
			
		||||
                            'flex flex-col border-l-2 border-transparent',
 | 
			
		||||
                        ])>
 | 
			
		||||
                            <div>Frequency: {{ $backup->frequency }}
 | 
			
		||||
                                ({{ data_get($backup->server(), 'settings.server_timezone', 'Instance timezone') }})
 | 
			
		||||
                    <div @class([
 | 
			
		||||
                        'flex flex-col border-l-2 transition-colors p-4 cursor-pointer bg-white hover:bg-gray-100 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 text-black dark:text-white',
 | 
			
		||||
                        'bg-gray-200 dark:bg-coolgray-200' =>
 | 
			
		||||
                            data_get($backup, 'id') === data_get($selectedBackup, 'id'),
 | 
			
		||||
                        'border-blue-500/50 border-dashed' =>
 | 
			
		||||
                            $backup->latest_log &&
 | 
			
		||||
                            data_get($backup->latest_log, 'status') === 'running',
 | 
			
		||||
                        'border-error' =>
 | 
			
		||||
                            $backup->latest_log &&
 | 
			
		||||
                            data_get($backup->latest_log, 'status') === 'failed',
 | 
			
		||||
                        'border-success' =>
 | 
			
		||||
                            $backup->latest_log &&
 | 
			
		||||
                            data_get($backup->latest_log, 'status') === 'success',
 | 
			
		||||
                        'border-gray-200 dark:border-coolgray-300' => !$backup->latest_log,
 | 
			
		||||
                        'border-coollabs' =>
 | 
			
		||||
                            data_get($backup, 'id') === data_get($selectedBackup, 'id'),
 | 
			
		||||
                    ]) wire:click="setSelectedBackup('{{ data_get($backup, 'id') }}')">
 | 
			
		||||
                        @if ($backup->latest_log && data_get($backup->latest_log, 'status') === 'running')
 | 
			
		||||
                            <div class="absolute top-2 right-2">
 | 
			
		||||
                                <x-loading />
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div>
 | 
			
		||||
                        @endif
 | 
			
		||||
                        <div class="flex items-center gap-2 mb-2">
 | 
			
		||||
                            @if ($backup->latest_log)
 | 
			
		||||
                                <span @class([
 | 
			
		||||
                                    'px-3 py-1 rounded-md text-xs font-medium tracking-wide shadow-xs',
 | 
			
		||||
                                    'bg-blue-100/80 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300 dark:shadow-blue-900/5' =>
 | 
			
		||||
                                        data_get($backup->latest_log, 'status') === 'running',
 | 
			
		||||
                                    'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200 dark:shadow-red-900/5' =>
 | 
			
		||||
                                        data_get($backup->latest_log, 'status') === 'failed',
 | 
			
		||||
                                    'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200 dark:shadow-green-900/5' =>
 | 
			
		||||
                                        data_get($backup->latest_log, 'status') === 'success',
 | 
			
		||||
                                ])>
 | 
			
		||||
                                    @php
 | 
			
		||||
                                        $statusText = match (data_get($backup->latest_log, 'status')) {
 | 
			
		||||
                                            'success' => 'Success',
 | 
			
		||||
                                            'running' => 'In Progress',
 | 
			
		||||
                                            'failed' => 'Failed',
 | 
			
		||||
                                            default => ucfirst(data_get($backup->latest_log, 'status')),
 | 
			
		||||
                                        };
 | 
			
		||||
                                    @endphp
 | 
			
		||||
                                    {{ $statusText }}
 | 
			
		||||
                                </span>
 | 
			
		||||
                            @else
 | 
			
		||||
                                <span
 | 
			
		||||
                                    class="px-3 py-1 rounded-md text-xs font-medium tracking-wide shadow-xs bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-200">
 | 
			
		||||
                                    No executions yet
 | 
			
		||||
                                </span>
 | 
			
		||||
                            @endif
 | 
			
		||||
                            <h3 class="font-semibold">{{ $backup->frequency }} Backup</h3>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="text-gray-600 dark:text-gray-400 text-sm">
 | 
			
		||||
                            @if ($backup->latest_log)
 | 
			
		||||
                                Started:
 | 
			
		||||
                                {{ formatDateInServerTimezone(data_get($backup->latest_log, 'created_at'), $backup->server()) }}
 | 
			
		||||
                                @if (data_get($backup->latest_log, 'status') !== 'running')
 | 
			
		||||
                                    <br>Ended:
 | 
			
		||||
                                    {{ formatDateInServerTimezone(data_get($backup->latest_log, 'finished_at'), $backup->server()) }}
 | 
			
		||||
                                    <br>Duration:
 | 
			
		||||
                                    {{ calculateDuration(data_get($backup->latest_log, 'created_at'), data_get($backup->latest_log, 'finished_at')) }}
 | 
			
		||||
                                    <br>Finished
 | 
			
		||||
                                    {{ \Carbon\Carbon::parse(data_get($backup->latest_log, 'finished_at'))->diffForHumans() }}
 | 
			
		||||
                                @endif
 | 
			
		||||
                                <br><br>Total Executions: {{ $backup->executions()->count() }}
 | 
			
		||||
                                @if ($backup->save_s3)
 | 
			
		||||
                                    <br>S3 Storage: Enabled
 | 
			
		||||
                                @endif
 | 
			
		||||
                                @php
 | 
			
		||||
                                    $successCount = $backup->executions()->where('status', 'success')->count();
 | 
			
		||||
                                    $totalCount = $backup->executions()->count();
 | 
			
		||||
                                    $successRate = $totalCount > 0 ? round(($successCount / $totalCount) * 100) : 0;
 | 
			
		||||
                                @endphp
 | 
			
		||||
                                @if ($totalCount > 0)
 | 
			
		||||
                                    <br>Success Rate: <span @class([
 | 
			
		||||
                                        'font-medium',
 | 
			
		||||
                                        'text-green-600' => $successRate >= 80,
 | 
			
		||||
                                        'text-yellow-600' => $successRate >= 50 && $successRate < 80,
 | 
			
		||||
                                        'text-red-600' => $successRate < 50,
 | 
			
		||||
                                    ])>{{ $successRate }}%</span>
 | 
			
		||||
                                    ({{ $successCount }}/{{ $totalCount }})
 | 
			
		||||
                                @endif
 | 
			
		||||
                                @if (data_get($backup->latest_log, 'status') === 'success')
 | 
			
		||||
                                    @php
 | 
			
		||||
                                        $size = data_get($backup->latest_log, 'size', 0);
 | 
			
		||||
                                        $sizeFormatted =
 | 
			
		||||
                                            $size > 0 ? number_format($size / 1024 / 1024, 2) . ' MB' : 'Unknown';
 | 
			
		||||
                                    @endphp
 | 
			
		||||
                                    <br>Last Backup Size: {{ $sizeFormatted }}
 | 
			
		||||
                                @endif
 | 
			
		||||
                            @else
 | 
			
		||||
                                Last Run: Never
 | 
			
		||||
                                <br>Total Executions: 0
 | 
			
		||||
                                @if ($backup->save_s3)
 | 
			
		||||
                                    <br>S3 Storage: Enabled
 | 
			
		||||
                                @endif
 | 
			
		||||
                            @endif
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                @endif
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user