++ consolidamento funzionalità Backup

This commit is contained in:
2026-07-29 16:34:11 +02:00
parent fd15903c9c
commit 7ac454cd25
16 changed files with 585 additions and 30 deletions

View File

@@ -52,8 +52,17 @@ class Profile extends Component
]);
// Il cast `hashed` sul modello User cifra il valore in fase di salvataggio.
//
// Si spegne anche `password_change_required`: oggi chi ha quel flag
// viene dirottato dal middleware e questa pagina non la raggiunge
// nemmeno, ma la condizione che il flag esprime — "questa password non
// e' piu' un segreto" — la soddisfa qualunque cambio, non solo quello
// fatto dalla schermata obbligatoria. Lasciarlo acceso qui
// significherebbe che un domani, allentando il middleware, l'utente
// resterebbe bloccato dopo aver gia' fatto quello che gli si chiedeva.
auth()->user()->update([
'password' => $this->password,
'password_change_required' => false,
]);
$this->reset(['current_password', 'password', 'password_confirmation']);

View File

@@ -48,15 +48,29 @@ class BackupManager extends Component
*/
public string $passwordConferma = '';
/** Messaggio mostrato in cima quando un avvio viene rifiutato. */
public string $erroreOperazione = '';
public function mount(): void
{
$this->authorize('settings.manage');
// Riaggancia un'operazione gia' in corso. Senza, ricaricare la pagina
// durante un backup lungo farebbe sparire la barra, e un azzeramento
// fermo in attesa di conferma diventerebbe irraggiungibile: nessuno
// potrebbe piu' ne' confermarlo ne' annullarlo.
$this->operationId = app(BackupOperationState::class)->corrente();
$this->aggiorna();
}
public function avviaBackup(): void
{
$this->authorize('settings.manage');
if (! $this->postoLibero()) {
return;
}
$this->operationId = $this->nuovaOperazione(BackupOperationState::TIPO_BACKUP);
CreateBackupArchive::dispatch($this->operationId, false);
@@ -64,6 +78,32 @@ class BackupManager extends Component
$this->aggiorna();
}
/**
* Vero se non c'e' gia' un'operazione in corso.
*
* Backup, ripristino e azzeramento lavorano tutti sullo stesso database e
* sullo stesso disco: farne partire due insieme — due schede, due
* amministratori — non li rallenta soltanto, li fa sovrascrivere a vicenda.
*/
private function postoLibero(): bool
{
$inCorso = app(BackupOperationState::class)->corrente();
if ($inCorso === null) {
$this->erroreOperazione = '';
return true;
}
// Si aggancia a quella in corso invece di limitarsi a rifiutare: chi ha
// premuto il pulsante vuole comunque vedere a che punto e'.
$this->operationId = $inCorso;
$this->aggiorna();
$this->erroreOperazione = 'C\'e\' gia\' un\'operazione in corso: attendi che finisca prima di avviarne un\'altra.';
return false;
}
/**
* Primo tempo dell'azzeramento: si costruisce il backup e lo si scarica.
* La cancellazione non parte da qui — serve il secondo tempo,
@@ -79,6 +119,10 @@ class BackupManager extends Component
'confermaReset.required' => 'Scrivi "' . self::CONFERMA_AZZERAMENTO . '" per confermare.']
);
if (! $this->postoLibero()) {
return;
}
$this->confermaReset = '';
$this->operationId = $this->nuovaOperazione(BackupOperationState::TIPO_RESET);
@@ -316,6 +360,21 @@ class BackupManager extends Component
&& in_array($this->operazione['status'] ?? '', ['queued', 'running'], true);
}
/**
* Ogni quanto reinterrogare lo stato.
*
* Mentre un job lavora servono aggiornamenti fitti, la barra deve muoversi.
* In attesa-download invece si aspetta una persona, che puo' metterci
* minuti: continuare a bussare ogni 750ms vorrebbe dire migliaia di
* richieste inutili se qualcuno lascia la scheda aperta e se ne va.
*/
public function getIntervalloPollProperty(): string
{
return ($this->operazione['fase'] ?? null) === BackupOperationState::FASE_ATTESA_DOWNLOAD
? '5s'
: '750ms';
}
public function render()
{
return view('livewire.settings.backup-manager');

View File

@@ -5,6 +5,8 @@ namespace App\Livewire\Territori;
use Livewire\Component;
use Livewire\WithPagination;
use App\Models\Territorio;
use App\Services\TerritorioThumbnailService;
use Illuminate\Support\Facades\Storage;
class TerritorioCestino extends Component
{
@@ -36,10 +38,36 @@ class TerritorioCestino extends Component
activity()->causedBy(auth()->user())
->withProperties(['numero' => $territorio->numero])
->log('force_deleted_territorio');
// I file vanno via con la riga. Cancellare solo il record lascerebbe la
// mappa e la miniatura sul volume senza piu' nulla che le riferisca:
// invisibili in ogni schermata, non raggiungibili da nessuna
// cancellazione futura, e comunque dentro ogni backup successivo.
// Prima della riga, non dopo: se la cancellazione dei file fallisce,
// il territorio resta nel cestino e l'operazione si puo' ritentare.
$this->rimuoviFile($territorio);
$territorio->forceDelete();
session()->flash('success', 'Territorio eliminato definitivamente.');
}
/**
* Rimuove mappa e miniatura di un territorio dal disco `public`.
*
* Stessa coppia di cancellazioni che fa TerritorioEdit quando si sostituisce
* il PDF; qui e' definitiva perche' non resta una riga a cui riattaccarle.
*/
private function rimuoviFile(Territorio $territorio): void
{
if ($territorio->pdf_path) {
Storage::disk('public')->delete($territorio->pdf_path);
}
if ($territorio->thumbnail_path) {
app(TerritorioThumbnailService::class)->delete($territorio->thumbnail_path);
}
}
public function render()
{
return view('livewire.territori.territorio-cestino', [