++ 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
|
@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’altra operazione è in corso: attendi che finisca.</p>
|
<p class="text-xs text-gray-500">Un’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 ────────────────────────────────────────── --}}
|
||||||
|
|||||||
Reference in New Issue
Block a user