++ 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:
@@ -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’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 ────────────────────────────────────────── --}}
|
||||
|
||||
Reference in New Issue
Block a user