++ add reset program

This commit is contained in:
2026-07-29 14:26:36 +02:00
parent c6abef977d
commit 588910b893
14 changed files with 1524 additions and 183 deletions

View File

@@ -0,0 +1,249 @@
<?php
namespace App\Livewire\Settings;
use App\Jobs\CreateBackupArchive;
use App\Jobs\ResetApplication;
use App\Services\BackupOperationState;
use Livewire\Component;
/**
* Avvio e monitoraggio delle operazioni lunghe: backup, ripristino,
* azzeramento.
*
* Il lavoro vero sta nei job; qui si avvia e si interroga lo stato. La barra
* mostra `processed/total` dell'operazione in corso, cioe' unita' di lavoro
* davvero completate — file finiti nell'archivio, file estratti, tabelle
* svuotate — non un'animazione a tempo.
*/
class BackupManager extends Component
{
/** Operazione seguita in questo momento; null quando non c'e' nulla in corso. */
public ?string $operationId = null;
public array $operazione = [];
/** Frase da digitare per sbloccare l'azzeramento. */
public const CONFERMA_AZZERAMENTO = 'AZZERA TUTTO';
public string $confermaReset = '';
public function mount(): void
{
$this->authorize('settings.manage');
}
public function avviaBackup(): void
{
$this->authorize('settings.manage');
$this->operationId = $this->nuovaOperazione(BackupOperationState::TIPO_BACKUP);
CreateBackupArchive::dispatch($this->operationId, false);
$this->aggiorna();
}
/**
* Primo tempo dell'azzeramento: si costruisce il backup e lo si scarica.
* La cancellazione non parte da qui — serve il secondo tempo,
* confermaAzzeramento(), a valle del download.
*/
public function avviaAzzeramento(): void
{
$this->authorize('settings.manage');
$this->validate(
['confermaReset' => 'required|in:' . self::CONFERMA_AZZERAMENTO],
['confermaReset.in' => 'Scrivi esattamente "' . self::CONFERMA_AZZERAMENTO . '" per confermare.',
'confermaReset.required' => 'Scrivi "' . self::CONFERMA_AZZERAMENTO . '" per confermare.']
);
$this->confermaReset = '';
$this->operationId = $this->nuovaOperazione(BackupOperationState::TIPO_RESET);
CreateBackupArchive::dispatch($this->operationId, true);
$this->aggiorna();
}
/**
* Secondo tempo dell'azzeramento: parte solo su conferma esplicita, dopo
* che l'archivio e' stato servito.
*
* La conferma umana non e' un doppione del `wire:confirm` iniziale: e' il
* solo punto in cui si sa che il backup e' davvero sul computer di chi sta
* cancellando. Il server puo' verificare di averlo *spedito*
* (`servito_at`), non che sia arrivato: nginx bufferizza la risposta, il
* processo PHP la considera scritta e un download annullato dal browser
* resterebbe indistinguibile da uno riuscito. Chiedere all'operatore e'
* l'unico controllo che copre anche quel caso.
*/
public function confermaAzzeramento(): void
{
$this->authorize('settings.manage');
$this->aggiorna();
if (($this->operazione['fase'] ?? null) !== BackupOperationState::FASE_ATTESA_DOWNLOAD) {
$this->addError('confermaReset', 'Nessun azzeramento in attesa di conferma.');
return;
}
if (($this->operazione['result']['servito_at'] ?? null) === null) {
$this->addError('confermaReset', 'Scarica prima il backup: senza, l\'azzeramento non parte.');
return;
}
app(BackupOperationState::class)->update($this->operationId, function (array $state) {
$state['fase'] = 'Azzeramento in avvio';
$state['processed'] = 0;
$state['total'] = 0;
return $state;
});
ResetApplication::dispatch($this->operationId, (int) auth()->id());
$this->aggiorna();
}
/**
* Annulla un azzeramento fermo in attesa di conferma. Il backup resta
* scaricabile: e' comunque un backup valido.
*/
public function annullaAzzeramento(): void
{
$this->authorize('settings.manage');
if (($this->operazione['fase'] ?? null) !== BackupOperationState::FASE_ATTESA_DOWNLOAD) {
return;
}
app(BackupOperationState::class)->markCompleted($this->operationId, [
'messaggio' => 'Azzeramento annullato. Il backup appena creato resta scaricabile.',
]);
// Da qui in poi e' un backup come un altro: la scheda mostra il
// pulsante di download, non piu' la conferma di cancellazione.
app(BackupOperationState::class)->update($this->operationId, function (array $state) {
$state['tipo'] = BackupOperationState::TIPO_BACKUP;
return $state;
});
$this->aggiorna();
}
/**
* Aggancia un'operazione avviata fuori da Livewire (il ripristino, che
* parte da un upload XHR e restituisce il suo id in JSON).
*/
public function seguiOperazione(string $operationId): void
{
$this->authorize('settings.manage');
$this->operationId = $operationId;
$this->aggiorna();
}
public function aggiorna(): void
{
if (! $this->operationId) {
return;
}
$stato = app(BackupOperationState::class)->get($this->operationId);
if (! $stato) {
$this->operationId = null;
$this->operazione = [];
return;
}
$this->operazione = $stato;
// Il download parte da solo appena l'archivio e' pronto: per il backup
// e' una comodita', per l'azzeramento e' il passaggio che sblocca la
// cancellazione, quindi non puo' dipendere da un clic.
if ($this->scaricaOra()) {
$this->dispatch('scarica-backup', url: route('backup.file', $this->operationId));
}
}
public function chiudi(): void
{
$this->operationId = null;
$this->operazione = [];
}
/**
* True una volta sola per operazione: l'evento di download va emesso quando
* l'archivio compare, non a ogni poll successivo.
*/
private function scaricaOra(): bool
{
$pronto = ($this->operazione['result']['file'] ?? null) !== null;
$giaInviato = $this->operazione['result']['download_inviato'] ?? false;
if (! $pronto || $giaInviato) {
return false;
}
app(BackupOperationState::class)->update($this->operationId, function (array $state) {
$state['result']['download_inviato'] = true;
return $state;
});
$this->operazione['result']['download_inviato'] = true;
return true;
}
private function nuovaOperazione(string $tipo): string
{
$operationId = (string) \Illuminate\Support\Str::uuid();
$stato = app(BackupOperationState::class);
$stato->initialize($operationId, $tipo, 'In coda');
$stato->log($operationId, 'Operazione creata. In attesa del worker di coda.');
return $operationId;
}
/** Percentuale della fase corrente, 0-100. */
public function getPercentualeProperty(): int
{
$stato = $this->operazione['status'] ?? null;
if ($stato === 'completed') {
return 100;
}
$total = (int) ($this->operazione['total'] ?? 0);
if ($total <= 0) {
return 0;
}
$processed = (int) ($this->operazione['processed'] ?? 0);
return (int) min(100, round($processed / $total * 100));
}
/** Se true la pagina continua a interrogare lo stato. */
public function getInCorsoProperty(): bool
{
return $this->operationId !== null
&& in_array($this->operazione['status'] ?? '', ['queued', 'running'], true);
}
public function render()
{
return view('livewire.settings.backup-manager');
}
}