++ console di log su backup e ripristino

Aggiunge un pannello che registra ogni passaggio del caricamento: file
scelto con la sua dimensione, limite del server, avanzamento ogni 10%,
codice HTTP della risposta, errori, timeout e interruzioni. Sotto, i log
lato server dell'operazione in corso.

Serve perche' i modi in cui un caricamento muore sono quasi tutti muti:
superato il limite nginx chiude la connessione dopo aver accettato zero
byte, e nessun evento porta un messaggio. Senza un diario, la barra si
pianta a una percentuale qualsiasi e la pagina non ha niente da dire. Un
battito ogni 15 secondi distingue "lento" da "fermo".

Le virgolette doppie restano bandite dentro l'x-data: chiudono
l'attributo HTML anche dentro un commento, e hanno gia' riversato il
codice a video due volte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-29 18:20:45 +02:00
parent c1dc47b3fd
commit f909791378

View File

@@ -21,17 +21,22 @@
@endphp @endphp
{{-- {{--
Il comportamento del riquadro di ripristino sta in un componente Alpine ATTENZIONE, prima di toccare l'x-data qui sotto.
registrato piu' sotto (@script), non in un x-data lungo.
Il motivo e' concreto: un attributo HTML finisce alla prima virgoletta Un attributo HTML finisce alla PRIMA virgoletta doppia, comprese quelle
doppia, comprese quelle dentro un commento. Con il codice nell'attributo, un dentro un commento JavaScript. Un banale // un generico "errore" ha gia'
banale // un generico "errore" ha chiuso l'attributo a meta' e ha riversato chiuso l'attributo a meta' e riversato tutto il codice rimanente a video
tutto il resto del JavaScript a video come testo. Dentro un blocco <script> come testo, due volte. Dentro questo blocco NON possono comparire
virgolette e apostrofi non hanno alcun potere di rompere il markup. virgolette doppie, da nessuna parte, neanche nei commenti.
Le stringhe usano comunque i backtick: e' la forma in cui un apostrofo Per lo stesso motivo le stringhe usano apici o backtick, e gli apostrofi
italiano non richiede alcun accorgimento. italiani stanno nei backtick, dove non richiedono alcun accorgimento: la
sequenza \' scritta a mano nel sorgente Blade chiude la stringa invece di
proteggere l'apostrofo, ed e' l'altro modo in cui questo blocco si e' gia'
rotto.
Un blocco <script> con @script eviterebbe la questione alla radice, ma
rompe il rilevamento della radice singola di Livewire (errore 500).
--}} --}}
<div class="space-y-6" <div class="space-y-6"
x-data="{ x-data="{
@@ -43,6 +48,57 @@
erroreUpload: '', erroreUpload: '',
passwordAzzeramento: '', passwordAzzeramento: '',
/*
* Diario di quel che succede nel browser.
*
* Serve perche' i modi in cui un caricamento muore sono spesso muti: il
* server chiude la connessione e nessun evento porta un messaggio, oppure
* la barra smette di avanzare senza che nulla lo dichiari. Qui ogni
* passaggio lascia una riga con l'ora, cosi' il punto esatto in cui si
* ferma e' leggibile invece che da indovinare, e il testo si puo' copiare
* e mandare a chi assiste.
*/
console: [],
ultimoAvanzamento: 0,
battito: null,
log(riga) {
const ora = new Date().toTimeString().slice(0, 8);
this.console.push(`[${ora}] ${riga}`);
// Il pannello e' alto qualche riga: il fondo e' la parte che interessa.
this.$nextTick(() => {
const el = this.$refs.consoleBox;
if (el) el.scrollTop = el.scrollHeight;
});
},
get consoleTesto() {
return this.console.join('\n');
},
/*
* Un caricamento fermo non genera eventi: senza qualcosa che batta il
* tempo, l'ultima riga resterebbe quella di dieci minuti prima e non si
* distinguerebbe 'lento' da 'morto'.
*/
avviaBattito() {
this.fermaBattito();
this.ultimoAvanzamento = Date.now();
this.battito = setInterval(() => {
const fermoDa = Math.round((Date.now() - this.ultimoAvanzamento) / 1000);
if (fermoDa >= 15) {
this.log(`...nessun avanzamento da ${fermoDa}s (${this.uploadPercento}%)`);
}
}, 15000);
},
fermaBattito() {
if (this.battito) {
clearInterval(this.battito);
this.battito = null;
}
},
azzera() { azzera() {
if (!this.passwordAzzeramento) { if (!this.passwordAzzeramento) {
return; return;
@@ -89,8 +145,15 @@
? `${file.name} (${(file.size / 1000000).toFixed(1)} MB)` ? `${file.name} (${(file.size / 1000000).toFixed(1)} MB)`
: ''; : '';
if (file) { if (!file) {
this.zipTroppoGrande(file); return;
}
this.log(`File scelto: ${file.name} — ${(file.size / 1000000).toFixed(1)} MB`);
this.log(`Limite del server: ${(this.maxByte / 1000000).toFixed(0)} MB`);
if (this.zipTroppoGrande(file)) {
this.log(`RIFIUTATO: supera il limite. Nessun caricamento avviato.`);
} }
}, },
@@ -99,6 +162,7 @@
const file = form.querySelector('input[name=backupZip]').files[0]; const file = form.querySelector('input[name=backupZip]').files[0];
if (this.zipTroppoGrande(file)) { if (this.zipTroppoGrande(file)) {
this.log(`RIFIUTATO: supera il limite. Nessun caricamento avviato.`);
return; return;
} }
@@ -106,6 +170,8 @@
this.uploadInCorso = true; this.uploadInCorso = true;
this.uploadPercento = 0; this.uploadPercento = 0;
this.log(`Invio a ${form.action}`);
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open('POST', form.action); xhr.open('POST', form.action);
xhr.setRequestHeader('Accept', 'application/json'); xhr.setRequestHeader('Accept', 'application/json');
@@ -116,26 +182,47 @@
// in attesa per sempre. // in attesa per sempre.
xhr.timeout = 300000; xhr.timeout = 300000;
let ultimaTacca = -1;
xhr.upload.addEventListener('progress', (e) => { xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) { if (!e.lengthComputable) {
this.uploadPercento = Math.round((e.loaded / e.total) * 100); return;
} }
this.uploadPercento = Math.round((e.loaded / e.total) * 100);
this.ultimoAvanzamento = Date.now();
// Una riga ogni 10%: abbastanza per vedere dove si ferma, non
// tanta da seppellire il resto.
const tacca = Math.floor(this.uploadPercento / 10);
if (tacca > ultimaTacca) {
ultimaTacca = tacca;
this.log(`Caricamento ${this.uploadPercento}% (${(e.loaded / 1000000).toFixed(1)} di ${(e.total / 1000000).toFixed(1)} MB)`);
}
});
xhr.upload.addEventListener('load', () => {
this.log(`Corpo della richiesta inviato per intero. In attesa della risposta del server...`);
}); });
xhr.addEventListener('load', () => { xhr.addEventListener('load', () => {
this.uploadInCorso = false; this.uploadInCorso = false;
this.fermaBattito();
this.log(`Risposta HTTP ${xhr.status} (${xhr.responseText.length} byte)`);
let payload = {}; let payload = {};
try { try {
payload = JSON.parse(xhr.responseText || '{}'); payload = JSON.parse(xhr.responseText || '{}');
} catch (e) { } catch (e) {
payload = {}; payload = {};
this.log(`La risposta non e' JSON. Inizio: ${xhr.responseText.slice(0, 120)}`);
} }
if (xhr.status >= 200 && xhr.status < 300 && payload.operation_id) { if (xhr.status >= 200 && xhr.status < 300 && payload.operation_id) {
this.uploadPercento = 100; this.uploadPercento = 100;
this.zipSelezionato = ''; this.zipSelezionato = '';
form.reset(); form.reset();
this.log(`Ripristino accodato: operazione ${payload.operation_id}`);
this.log(`Da qui l'avanzamento arriva dal server.`);
// Da qui in poi l'avanzamento arriva dal job, non dall'upload. // Da qui in poi l'avanzamento arriva dal job, non dall'upload.
this.$wire.seguiOperazione(payload.operation_id); this.$wire.seguiOperazione(payload.operation_id);
return; return;
@@ -145,27 +232,35 @@
// il ripristino sia partito. // il ripristino sia partito.
this.uploadPercento = 0; this.uploadPercento = 0;
this.erroreUpload = this.messaggioErrore(xhr.status, payload); this.erroreUpload = this.messaggioErrore(xhr.status, payload);
this.log(`ERRORE: ${this.erroreUpload}`);
}); });
xhr.addEventListener('error', () => { xhr.addEventListener('error', () => {
this.uploadInCorso = false; this.uploadInCorso = false;
this.uploadPercento = 0; this.uploadPercento = 0;
this.fermaBattito();
this.erroreUpload = `Errore di rete durante il caricamento: connessione interrotta dal ` this.erroreUpload = `Errore di rete durante il caricamento: connessione interrotta dal `
+ `server. Succede tipicamente quando il pacchetto supera il limite accettato.`; + `server. Succede tipicamente quando il pacchetto supera il limite accettato.`;
this.log(`ERRORE DI RETE: il server ha chiuso la connessione senza rispondere.`);
}); });
xhr.addEventListener('timeout', () => { xhr.addEventListener('timeout', () => {
this.uploadInCorso = false; this.uploadInCorso = false;
this.uploadPercento = 0; this.uploadPercento = 0;
this.fermaBattito();
this.erroreUpload = `Il caricamento non ha risposto entro 5 minuti ed è stato interrotto.`; this.erroreUpload = `Il caricamento non ha risposto entro 5 minuti ed è stato interrotto.`;
this.log(`TIMEOUT dopo 5 minuti senza risposta.`);
}); });
xhr.addEventListener('abort', () => { xhr.addEventListener('abort', () => {
this.uploadInCorso = false; this.uploadInCorso = false;
this.uploadPercento = 0; this.uploadPercento = 0;
this.fermaBattito();
this.erroreUpload = `Caricamento annullato.`; this.erroreUpload = `Caricamento annullato.`;
this.log(`Caricamento annullato.`);
}); });
this.avviaBattito();
xhr.send(new FormData(form)); xhr.send(new FormData(form));
}, },
@@ -400,6 +495,40 @@
<p class="text-xs text-gray-500">Un&rsquo;altra operazione è in corso: attendi che finisca.</p> <p class="text-xs text-gray-500">Un&rsquo;altra operazione è in corso: attendi che finisca.</p>
@endif @endif
</form> </form>
{{-- Console: il diario del browser e quello del server, uno sotto
l'altro. E' l'unico posto da cui si capisce dove un caricamento si
e' fermato, e il testo si seleziona e si copia per mandarlo a chi
assiste. --}}
<div class="border-t border-gray-200 pt-4">
<div class="flex items-center justify-between gap-3">
<h3 class="text-sm font-semibold text-gray-900">Console</h3>
<div class="flex items-center gap-3 text-xs">
<button type="button" x-on:click="console = []" class="text-gray-600 underline">Pulisci</button>
<button type="button"
x-on:click="navigator.clipboard.writeText(consoleTesto + String.fromCharCode(10) + $refs.logServer?.value)"
class="text-gray-600 underline">Copia tutto</button>
</div>
</div>
<div class="mt-2">
<div class="text-xs font-medium text-gray-700">Browser</div>
<textarea readonly rows="8" x-ref="consoleBox" x-text="consoleTesto || 'In attesa. Scegli un pacchetto per iniziare.'"
class="mt-1 block w-full rounded-lg border border-gray-300 bg-gray-950 px-3 py-2 font-mono text-xs leading-5 text-gray-100"></textarea>
</div>
<div class="mt-3">
<div class="text-xs font-medium text-gray-700">Server</div>
<textarea readonly rows="8" x-ref="logServer"
class="mt-1 block w-full rounded-lg border border-gray-300 bg-gray-950 px-3 py-2 font-mono text-xs leading-5 text-gray-100">{{ !empty($op['logs']) ? implode(PHP_EOL, $op['logs']) : 'Nessuna operazione sul server. Compare qui appena il pacchetto viene accettato.' }}</textarea>
</div>
<p class="mt-2 text-xs text-gray-500">
Limite di caricamento configurato:
<strong>{{ \App\Http\Controllers\Settings\BackupController::maxUploadMb() }} MB</strong>
(variabile <code class="rounded bg-gray-100 px-1">MAX_UPLOAD_MB</code>).
</p>
</div>
</div> </div>
{{-- ─── Zona pericolosa ────────────────────────────────────────── --}} {{-- ─── Zona pericolosa ────────────────────────────────────────── --}}