++ 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
{{--
Il comportamento del riquadro di ripristino sta in un componente Alpine
registrato piu' sotto (@script), non in un x-data lungo.
ATTENZIONE, prima di toccare l'x-data qui sotto.
Il motivo e' concreto: un attributo HTML finisce alla prima virgoletta
doppia, comprese quelle dentro un commento. Con il codice nell'attributo, un
banale // un generico "errore" ha chiuso l'attributo a meta' e ha riversato
tutto il resto del JavaScript a video come testo. Dentro un blocco <script>
virgolette e apostrofi non hanno alcun potere di rompere il markup.
Un attributo HTML finisce alla PRIMA virgoletta doppia, comprese quelle
dentro un commento JavaScript. Un banale // un generico "errore" ha gia'
chiuso l'attributo a meta' e riversato tutto il codice rimanente a video
come testo, due volte. Dentro questo blocco NON possono comparire
virgolette doppie, da nessuna parte, neanche nei commenti.
Le stringhe usano comunque i backtick: e' la forma in cui un apostrofo
italiano non richiede alcun accorgimento.
Per lo stesso motivo le stringhe usano apici o backtick, e gli apostrofi
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"
x-data="{
@@ -43,6 +48,57 @@
erroreUpload: '',
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() {
if (!this.passwordAzzeramento) {
return;
@@ -89,8 +145,15 @@
? `${file.name} (${(file.size / 1000000).toFixed(1)} MB)`
: '';
if (file) {
this.zipTroppoGrande(file);
if (!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];
if (this.zipTroppoGrande(file)) {
this.log(`RIFIUTATO: supera il limite. Nessun caricamento avviato.`);
return;
}
@@ -106,6 +170,8 @@
this.uploadInCorso = true;
this.uploadPercento = 0;
this.log(`Invio a ${form.action}`);
const xhr = new XMLHttpRequest();
xhr.open('POST', form.action);
xhr.setRequestHeader('Accept', 'application/json');
@@ -116,26 +182,47 @@
// in attesa per sempre.
xhr.timeout = 300000;
let ultimaTacca = -1;
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
this.uploadPercento = Math.round((e.loaded / e.total) * 100);
if (!e.lengthComputable) {
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', () => {
this.uploadInCorso = false;
this.fermaBattito();
this.log(`Risposta HTTP ${xhr.status} (${xhr.responseText.length} byte)`);
let payload = {};
try {
payload = JSON.parse(xhr.responseText || '{}');
} catch (e) {
payload = {};
this.log(`La risposta non e' JSON. Inizio: ${xhr.responseText.slice(0, 120)}`);
}
if (xhr.status >= 200 && xhr.status < 300 && payload.operation_id) {
this.uploadPercento = 100;
this.zipSelezionato = '';
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.
this.$wire.seguiOperazione(payload.operation_id);
return;
@@ -145,27 +232,35 @@
// il ripristino sia partito.
this.uploadPercento = 0;
this.erroreUpload = this.messaggioErrore(xhr.status, payload);
this.log(`ERRORE: ${this.erroreUpload}`);
});
xhr.addEventListener('error', () => {
this.uploadInCorso = false;
this.uploadPercento = 0;
this.fermaBattito();
this.erroreUpload = `Errore di rete durante il caricamento: connessione interrotta dal `
+ `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', () => {
this.uploadInCorso = false;
this.uploadPercento = 0;
this.fermaBattito();
this.erroreUpload = `Il caricamento non ha risposto entro 5 minuti ed è stato interrotto.`;
this.log(`TIMEOUT dopo 5 minuti senza risposta.`);
});
xhr.addEventListener('abort', () => {
this.uploadInCorso = false;
this.uploadPercento = 0;
this.fermaBattito();
this.erroreUpload = `Caricamento annullato.`;
this.log(`Caricamento annullato.`);
});
this.avviaBattito();
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>
@endif
</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>
{{-- ─── Zona pericolosa ────────────────────────────────────────── --}}