What Implements tenant-scoped backup scheduling end-to-end: schedules CRUD, minute-based dispatch, queued execution, run history, manual “Run now/Retry”, retention (keep last N), and auditability. Key changes Filament UI: Backup Schedules resource with tenant scoping + SEC-002 role gating. Scheduler + queue: tenantpilot:schedules:dispatch command wired in scheduler (runs every minute), creates idempotent BackupScheduleRun records and dispatches jobs. Execution: RunBackupScheduleJob syncs policies, creates immutable backup sets, updates run status, writes audit logs, applies retry/backoff mapping, and triggers retention. Run history: Relation manager + “View” modal rendering run details. UX polish: row actions grouped; bulk actions grouped (run now / retry / delete). Bulk dispatch writes DB notifications (shows in notifications panel). Validation: policy type hard-validation on save; unknown policy types handled safely at runtime (skipped/partial). Tests: comprehensive Pest coverage for CRUD/scoping/validation, idempotency, job outcomes, error mapping, retention, view modal, run-now/retry notifications, bulk delete (incl. operator forbidden). Files / Areas Filament: BackupScheduleResource.php and app/Filament/Resources/BackupScheduleResource/* Scheduling/Jobs: app/Console/Commands/TenantpilotDispatchBackupSchedules.php, app/Jobs/RunBackupScheduleJob.php, app/Jobs/ApplyBackupScheduleRetentionJob.php, console.php Models/Migrations: app/Models/BackupSchedule.php, app/Models/BackupScheduleRun.php, database/migrations/backup_schedules, backup_schedule_runs Notifications: BackupScheduleRunDispatchedNotification.php Specs: specs/032-backup-scheduling-mvp/* (tasks/checklist/quickstart updates) How to test (Sail) Run tests: ./vendor/bin/sail artisan test tests/Feature/BackupScheduling Run formatter: ./vendor/bin/sail php ./vendor/bin/pint --dirty Apply migrations: ./vendor/bin/sail artisan migrate Manual dispatch: ./vendor/bin/sail artisan tenantpilot:schedules:dispatch Notes Uses DB notifications for queued UI actions to ensure they appear in the notifications panel even under queue fakes in tests. Checklist gate for 032 is PASS; tasks updated accordingly. Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local> Reviewed-on: #34
111 lines
4.6 KiB
PHP
111 lines
4.6 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Resources\BackupScheduleResource\RelationManagers;
|
|
|
|
use App\Filament\Resources\BackupSetResource;
|
|
use App\Models\BackupScheduleRun;
|
|
use App\Models\Tenant;
|
|
use Filament\Actions;
|
|
use Filament\Resources\RelationManagers\RelationManager;
|
|
use Filament\Tables;
|
|
use Filament\Tables\Table;
|
|
use Illuminate\Contracts\View\View;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
|
|
class BackupScheduleRunsRelationManager extends RelationManager
|
|
{
|
|
protected static string $relationship = 'runs';
|
|
|
|
public function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->modifyQueryUsing(fn (Builder $query) => $query->where('tenant_id', Tenant::current()->getKey())->with('backupSet'))
|
|
->defaultSort('scheduled_for', 'desc')
|
|
->columns([
|
|
Tables\Columns\TextColumn::make('scheduled_for')
|
|
->label('Scheduled for')
|
|
->dateTime(),
|
|
Tables\Columns\TextColumn::make('status')
|
|
->badge()
|
|
->color(fn (?string $state): string => match ($state) {
|
|
BackupScheduleRun::STATUS_SUCCESS => 'success',
|
|
BackupScheduleRun::STATUS_PARTIAL => 'warning',
|
|
BackupScheduleRun::STATUS_RUNNING => 'primary',
|
|
BackupScheduleRun::STATUS_SKIPPED => 'gray',
|
|
BackupScheduleRun::STATUS_FAILED,
|
|
BackupScheduleRun::STATUS_CANCELED => 'danger',
|
|
default => 'gray',
|
|
}),
|
|
Tables\Columns\TextColumn::make('duration')
|
|
->label('Duration')
|
|
->getStateUsing(function (BackupScheduleRun $record): string {
|
|
if (! $record->started_at || ! $record->finished_at) {
|
|
return '—';
|
|
}
|
|
|
|
$seconds = max(0, $record->started_at->diffInSeconds($record->finished_at));
|
|
|
|
if ($seconds < 60) {
|
|
return $seconds.'s';
|
|
}
|
|
|
|
$minutes = intdiv($seconds, 60);
|
|
$rem = $seconds % 60;
|
|
|
|
return sprintf('%dm %ds', $minutes, $rem);
|
|
}),
|
|
Tables\Columns\TextColumn::make('counts')
|
|
->label('Counts')
|
|
->getStateUsing(function (BackupScheduleRun $record): string {
|
|
$summary = is_array($record->summary) ? $record->summary : [];
|
|
|
|
$total = (int) ($summary['policies_total'] ?? 0);
|
|
$backedUp = (int) ($summary['policies_backed_up'] ?? 0);
|
|
$errors = (int) ($summary['errors_count'] ?? 0);
|
|
|
|
if ($total === 0 && $backedUp === 0 && $errors === 0) {
|
|
return '—';
|
|
}
|
|
|
|
return sprintf('%d/%d (%d errors)', $backedUp, $total, $errors);
|
|
}),
|
|
Tables\Columns\TextColumn::make('error_code')
|
|
->label('Error')
|
|
->badge()
|
|
->default('—'),
|
|
Tables\Columns\TextColumn::make('error_message')
|
|
->label('Message')
|
|
->default('—')
|
|
->limit(80)
|
|
->wrap(),
|
|
Tables\Columns\TextColumn::make('backup_set_id')
|
|
->label('Backup set')
|
|
->default('—')
|
|
->url(function (BackupScheduleRun $record): ?string {
|
|
if (! $record->backup_set_id) {
|
|
return null;
|
|
}
|
|
|
|
return BackupSetResource::getUrl('view', ['record' => $record->backup_set_id], tenant: Tenant::current());
|
|
})
|
|
->openUrlInNewTab(true),
|
|
])
|
|
->filters([])
|
|
->headerActions([])
|
|
->actions([
|
|
Actions\Action::make('view')
|
|
->label('View')
|
|
->icon('heroicon-o-eye')
|
|
->modalHeading('View backup schedule run')
|
|
->modalSubmitAction(false)
|
|
->modalCancelActionLabel('Close')
|
|
->modalContent(function (BackupScheduleRun $record): View {
|
|
return view('filament.modals.backup-schedule-run-view', [
|
|
'run' => $record,
|
|
]);
|
|
}),
|
|
])
|
|
->bulkActions([]);
|
|
}
|
|
}
|