TenantAtlas/app/Console/Commands/TenantpilotPurgeNonPersistentData.php
ahmido a97beefda3 056-remove-legacy-bulkops (#65)
Kurzbeschreibung

Versteckt die Rerun-Row-Action für archivierte (soft-deleted) RestoreRuns und verhindert damit fehlerhafte Neu-Starts aus dem Archiv; ergänzt einen Regressionstest.
Änderungen

Code: RestoreRunResource.php — Sichtbarkeit der rerun-Action geprüft auf ! $record->trashed() und defensive Abbruchprüfung im Action-Handler.
Tests: RestoreRunRerunTest.php — neuer Test rerun action is hidden for archived restore runs.
Warum

Archivierte RestoreRuns durften nicht neu gestartet werden; UI zeigte trotzdem die Option. Das führte zu verwirrendem Verhalten und möglichen Fehlern beim Enqueueing.
Verifikation / QA

Unit/Feature:
./vendor/bin/sail artisan test tests/Feature/RestoreRunRerunTest.php
Stil/format:
./vendor/bin/pint --dirty
Manuell (UI):
Als Tenant-Admin Filament → Restore Runs öffnen.
Filter Archived aktivieren (oder Trashed filter auswählen).
Sicherstellen, dass für archivierte Einträge die Rerun-Action nicht sichtbar ist.
Auf einem aktiven (nicht-archivierten) Run prüfen, dass Rerun sichtbar bleibt und wie erwartet eine neue RestoreRun erzeugt.
Wichtige Hinweise

Kein DB-Migration required.
Diese PR enthält nur den UI-/Filament-Fix; die zuvor gemachten operative Fixes für Queue/adapter-Reconciliation bleiben ebenfalls auf dem Branch (z. B. frühere commits während der Debugging-Session).
T055 (Schema squash) wurde bewusst zurückgestellt und ist nicht Teil dieses PRs.
Merge-Checklist

 Tests lokal laufen (RestoreRunRerunTest grünt)
 Pint läuft ohne ungepatchte Fehler
 Branch gepusht: 056-remove-legacy-bulkops (PR-URL: https://git.cloudarix.de/ahmido/TenantAtlas/compare/dev...056-remove-legacy-bulkops)

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local>
Reviewed-on: #65
2026-01-19 23:27:52 +00:00

165 lines
5.1 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Models\AuditLog;
use App\Models\BackupItem;
use App\Models\BackupSchedule;
use App\Models\BackupScheduleRun;
use App\Models\BackupSet;
use App\Models\OperationRun;
use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\RestoreRun;
use App\Models\Tenant;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use RuntimeException;
class TenantpilotPurgeNonPersistentData extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'tenantpilot:purge-nonpersistent
{tenant? : Tenant id / tenant_id / external_id (defaults to current tenant)}
{--all : Purge for all tenants}
{--force : Actually delete rows}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Permanently delete non-persistent (regeneratable) tenant data like policies, backups, runs, and logs.';
/**
* Execute the console command.
*/
public function handle(): int
{
$tenants = $this->resolveTenants();
if ($tenants->isEmpty()) {
$this->error('No tenants selected. Provide {tenant} or use --all.');
return self::FAILURE;
}
$isDryRun = ! (bool) $this->option('force');
if ($isDryRun) {
$this->warn('Dry run: no rows will be deleted. Re-run with --force to apply.');
} else {
$this->warn('This will PERMANENTLY delete non-persistent tenant data.');
if ($this->input->isInteractive() && ! $this->confirm('Proceed?', false)) {
$this->info('Aborted.');
return self::SUCCESS;
}
}
foreach ($tenants as $tenant) {
$counts = $this->countsForTenant($tenant);
$this->line('');
$this->info("Tenant: {$tenant->id} ({$tenant->name})");
$this->table(
['Table', 'Rows'],
collect($counts)
->map(fn (int $count, string $table) => [$table, $count])
->values()
->all(),
);
if ($isDryRun) {
continue;
}
DB::transaction(function () use ($tenant): void {
BackupScheduleRun::query()
->where('tenant_id', $tenant->id)
->delete();
BackupSchedule::query()
->where('tenant_id', $tenant->id)
->delete();
OperationRun::query()
->where('tenant_id', $tenant->id)
->delete();
AuditLog::query()
->where('tenant_id', $tenant->id)
->delete();
RestoreRun::withTrashed()
->where('tenant_id', $tenant->id)
->forceDelete();
BackupItem::withTrashed()
->where('tenant_id', $tenant->id)
->forceDelete();
BackupSet::withTrashed()
->where('tenant_id', $tenant->id)
->forceDelete();
PolicyVersion::withTrashed()
->where('tenant_id', $tenant->id)
->forceDelete();
Policy::query()
->where('tenant_id', $tenant->id)
->delete();
});
$this->info('Purged.');
}
return self::SUCCESS;
}
private function resolveTenants()
{
if ((bool) $this->option('all')) {
return Tenant::query()->get();
}
$tenantArg = $this->argument('tenant');
if ($tenantArg !== null && $tenantArg !== '') {
$tenant = Tenant::query()->forTenant($tenantArg)->first();
return $tenant ? collect([$tenant]) : collect();
}
try {
return collect([Tenant::current()]);
} catch (RuntimeException) {
return collect();
}
}
/**
* @return array<string,int>
*/
private function countsForTenant(Tenant $tenant): array
{
return [
'backup_schedule_runs' => BackupScheduleRun::query()->where('tenant_id', $tenant->id)->count(),
'backup_schedules' => BackupSchedule::query()->where('tenant_id', $tenant->id)->count(),
'operation_runs' => OperationRun::query()->where('tenant_id', $tenant->id)->count(),
'audit_logs' => AuditLog::query()->where('tenant_id', $tenant->id)->count(),
'restore_runs' => RestoreRun::withTrashed()->where('tenant_id', $tenant->id)->count(),
'backup_items' => BackupItem::withTrashed()->where('tenant_id', $tenant->id)->count(),
'backup_sets' => BackupSet::withTrashed()->where('tenant_id', $tenant->id)->count(),
'policy_versions' => PolicyVersion::withTrashed()->where('tenant_id', $tenant->id)->count(),
'policies' => Policy::query()->where('tenant_id', $tenant->id)->count(),
];
}
}