TenantAtlas/app/Console/Commands/OpsReconcileAdapterRuns.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

117 lines
4.2 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Services\AdapterRunReconciler;
use Illuminate\Console\Command;
use Throwable;
class OpsReconcileAdapterRuns extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ops:reconcile-adapter-runs
{--type= : Adapter run type (e.g. restore.execute)}
{--tenant= : Tenant ID}
{--older-than=60 : Only consider runs older than N minutes}
{--dry-run=true : Preview only (true/false)}
{--limit=50 : Max number of runs to inspect}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Reconcile stale adapter-backed operation runs from DB-only source-of-truth records.';
/**
* Execute the console command.
*/
public function handle()
{
try {
/** @var AdapterRunReconciler $reconciler */
$reconciler = app(AdapterRunReconciler::class);
$type = $this->option('type');
$type = is_string($type) && trim($type) !== '' ? trim($type) : null;
$tenantId = $this->option('tenant');
$tenantId = is_numeric($tenantId) ? (int) $tenantId : null;
$olderThanMinutes = $this->option('older-than');
$olderThanMinutes = is_numeric($olderThanMinutes) ? (int) $olderThanMinutes : 60;
$olderThanMinutes = max(1, $olderThanMinutes);
$limit = $this->option('limit');
$limit = is_numeric($limit) ? (int) $limit : 50;
$limit = max(1, $limit);
$dryRun = $this->option('dry-run');
$dryRun = filter_var($dryRun, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE);
$dryRun = $dryRun ?? true;
$result = $reconciler->reconcile([
'type' => $type,
'tenant_id' => $tenantId,
'older_than_minutes' => $olderThanMinutes,
'limit' => $limit,
'dry_run' => $dryRun,
]);
$changes = $result['changes'] ?? [];
usort($changes, static fn (array $a, array $b): int => ((int) ($a['operation_run_id'] ?? 0)) <=> ((int) ($b['operation_run_id'] ?? 0)));
$this->info('Adapter run reconciliation');
$this->line('dry_run: '.($dryRun ? 'true' : 'false'));
$this->line('type: '.($type ?? '(all supported)'));
$this->line('tenant: '.($tenantId ? (string) $tenantId : '(all)'));
$this->line('older_than_minutes: '.$olderThanMinutes);
$this->line('limit: '.$limit);
$this->newLine();
$this->line('candidates: '.(int) ($result['candidates'] ?? 0));
$this->line('reconciled: '.(int) ($result['reconciled'] ?? 0));
$this->line('skipped: '.(int) ($result['skipped'] ?? 0));
$this->newLine();
if ($changes === []) {
$this->info('No changes.');
return self::SUCCESS;
}
$rows = [];
foreach ($changes as $change) {
$before = is_array($change['before'] ?? null) ? $change['before'] : [];
$after = is_array($change['after'] ?? null) ? $change['after'] : [];
$rows[] = [
'applied' => ($change['applied'] ?? false) ? 'yes' : 'no',
'operation_run_id' => (int) ($change['operation_run_id'] ?? 0),
'type' => (string) ($change['type'] ?? ''),
'source_id' => (int) ($change['restore_run_id'] ?? 0),
'before' => (string) (($before['status'] ?? '').'/'.($before['outcome'] ?? '')),
'after' => (string) (($after['status'] ?? '').'/'.($after['outcome'] ?? '')),
];
}
$this->table(
['applied', 'operation_run_id', 'type', 'source_id', 'before', 'after'],
$rows,
);
return self::SUCCESS;
} catch (Throwable $e) {
$this->error('Reconciliation failed: '.$e->getMessage());
return self::FAILURE;
}
}
}