diff --git a/app/Filament/Pages/Supporto/Modifiche.php b/app/Filament/Pages/Supporto/Modifiche.php index bbceec9..cc2cee4 100644 --- a/app/Filament/Pages/Supporto/Modifiche.php +++ b/app/Filament/Pages/Supporto/Modifiche.php @@ -5,6 +5,8 @@ use App\Models\Stabile; use App\Models\SupportBugRegistry; use App\Models\SupportUpdateEntry; +use App\Models\User; +use App\Services\ProgramAclService; use App\Support\StabileContext; use BackedEnum; use Filament\Notifications\Notification; @@ -228,20 +230,23 @@ class Modifiche extends Page /** @var array */ public array $manualSections = []; + /** @var array> */ + public array $invoiceExtractionProfiles = []; + public function mount(): void { + $this->updateSkipBackup = ! $this->shouldRunPreupdateBackup(); $this->reloadDashboardData(); } public static function canAccess(): bool { $user = Auth::user(); - if (! $user) { + if (! $user instanceof User) { return false; } - // Pagina informativa: accessibile a chi accede al pannello. - return true; + return app(ProgramAclService::class)->canAccessProgram($user, 'supporto.modifiche'); } public function canRunUpdate(): bool @@ -258,6 +263,7 @@ public function reloadDashboardData(): void $this->loadVersionInfo(); $this->loadLatestCommits(); $this->loadCommitNotes(); + $this->loadInvoiceExtractionProfiles(); $this->loadRecentFilamentPages(); $this->loadFunctionalHighlights(); $this->loadManualSections(); @@ -283,6 +289,28 @@ public function reloadDashboardData(): void } } + private function loadInvoiceExtractionProfiles(): void + { + $profiles = config('document_extraction_profiles.profiles', []); + $this->invoiceExtractionProfiles = collect(is_array($profiles) ? $profiles : []) + ->filter(fn($profile): bool => is_array($profile) && ! empty($profile['key'])) + ->map(function (array $profile): array { + return [ + 'key' => (string) ($profile['key'] ?? ''), + 'service' => (string) ($profile['service'] ?? ''), + 'provider' => (string) ($profile['provider'] ?? ''), + 'status' => (string) ($profile['status'] ?? 'draft'), + 'source' => (string) ($profile['source'] ?? ''), + 'fields' => array_values(array_filter(array_map('strval', (array) ($profile['fields'] ?? [])))), + 'usage' => array_values(array_filter(array_map('strval', (array) ($profile['usage'] ?? [])))), + 'notes' => trim((string) ($profile['notes'] ?? '')), + 'commands' => array_values(array_filter(array_map('strval', (array) ($profile['commands'] ?? [])))), + ]; + }) + ->values() + ->all(); + } + public function runUpdate(): void { if (! $this->canRunUpdate()) { @@ -329,6 +357,11 @@ public function runGitSync(): void } public function runNodeRefresh(): void + { + $this->runAutonomousNodeRefresh(); + } + + public function runAutonomousNodeRefresh(): void { if (! $this->canRunUpdate()) { Notification::make() @@ -340,6 +373,22 @@ public function runNodeRefresh(): void return; } + if ($this->updateInProgress) { + Notification::make() + ->title('Aggiornamento gia in corso') + ->body('Il nodo sta gia eseguendo un refresh asincrono. Attendi la fine o aggiorna l avanzamento.') + ->warning() + ->send(); + + return; + } + + if ($this->shouldUseDaemonUpdateExecutor()) { + $this->queueDaemonNodeRefreshRequest(); + + return; + } + $this->startNodeRefreshJob(); } @@ -435,6 +484,20 @@ public function refreshUpdateProgress(): void if ($exitCode !== 0) { $this->appendSupportEvent('update_async', 'Aggiornamento asincrono fallito', (string) ($this->lastUpdateOutput ?? '')); + + Notification::make() + ->title('Refresh nodo fallito') + ->body($this->lastUpdateIssue ?: 'Controlla output completo, log supporto e runtime errors.') + ->danger() + ->send(); + } else { + $this->appendSupportEvent('update_async', 'Aggiornamento asincrono completato', (string) ($this->lastUpdateOutput ?? '')); + + Notification::make() + ->title('Refresh nodo completato') + ->body('Il nodo e stato riallineato senza accesso al sistema remoto. Backup, maintenance, sync e rebuild risultano completati.') + ->success() + ->send(); } $this->reloadDashboardData(); @@ -578,21 +641,25 @@ public function runMaintenanceMigrate(): void */ public function getUpdatePlannedStepsProperty(): array { - $requireDrive = $this->shouldRequireDrivePreupdateBackup(); - $driveEnabled = $this->shouldAttemptDrivePreupdateBackup(); + $backupEnabled = $this->shouldRunPreupdateBackup(); + $backupVisible = $this->shouldShowPreupdateBackupPlan(); + $requireDrive = $this->shouldRequireDrivePreupdateBackup(); + $driveEnabled = $this->shouldAttemptDrivePreupdateBackup(); $steps = [ 'Verifica canale update: ' . $this->updateChannel, - $this->updateSkipBackup - ? 'Backup pre-update saltato manualmente (modalita test/debug)' - : 'Backup pre-update automatico (snapshot differenziale + indice record)', - $this->updateSkipBackup - ? 'Upload backup su Google Drive non eseguito perche il backup e stato saltato' - : ($requireDrive + $backupEnabled + ? 'Backup pre-update automatico (snapshot differenziale + indice record)' + : ($backupVisible + ? 'Backup pre-update previsto ma temporaneamente disattivato finche la procedura non e definitiva' + : 'Backup pre-update non previsto per questo nodo'), + $backupEnabled + ? ($requireDrive ? 'Upload backup su Google Drive obbligatorio prima dell update' : ($driveEnabled ? 'Upload backup su Google Drive tentato se configurato sul sito corrente' - : 'Upload backup su Google Drive non obbligatorio per questo nodo')), + : 'Upload backup su Google Drive non obbligatorio per questo nodo')) + : 'Integrazione Google Drive prevista solo come fase successiva; per ora non viene eseguita durante l aggiornamento', 'Esecuzione in modalita ' . ($this->updateDryRun ? 'dry-run (nessuna scrittura)' : 'apply (aggiornamento reale)'), 'Flag force: ' . ($this->updateForce ? 'abilitato' : 'disabilitato'), 'Check endpoint update consigliato prima del lancio', @@ -832,6 +899,8 @@ private function startUpdateJob(bool $fallback): void { $this->registerPlannedUpdateSummary(); + $backupEnabled = $this->shouldRunPreupdateBackup(); + $jobId = now()->format('YmdHis') . '-' . Str::lower(Str::random(6)); $jobsDir = storage_path('app/support/update-jobs'); if (! is_dir($jobsDir)) { @@ -844,17 +913,17 @@ private function startUpdateJob(bool $fallback): void $this->updateJobId = $jobId; $this->updateInProgress = true; $this->updateProgressPercent = 1; - $this->updateProgressMessage = $this->updateSkipBackup - ? 'Preparazione aggiornamento senza backup (modalita test)...' - : 'Preparazione backup pre-update...'; + $this->updateProgressMessage = $backupEnabled + ? 'Preparazione backup pre-update...' + : 'Preparazione aggiornamento con backup solo indicato...'; $this->updateProgressStatus = 'running'; @file_put_contents($progressPath, json_encode([ 'timestamp' => now()->toIso8601String(), 'percent' => 1, - 'message' => $this->updateSkipBackup - ? 'Preparazione aggiornamento senza backup (modalita test)...' - : 'Preparazione backup pre-update...', + 'message' => $backupEnabled + ? 'Preparazione backup pre-update...' + : 'Preparazione aggiornamento con backup solo indicato...', 'status' => 'running', 'exit_code' => null, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); @@ -863,7 +932,7 @@ private function startUpdateJob(bool $fallback): void $requireDrive = $this->shouldRequireDrivePreupdateBackup(); $driveEnabled = $this->shouldAttemptDrivePreupdateBackup(); - if ($this->updateSkipBackup) { + if (! $backupEnabled) { $requireDrive = false; $driveEnabled = false; } @@ -886,7 +955,7 @@ private function startUpdateJob(bool $fallback): void return; } - if (! $this->updateSkipBackup) { + if ($backupEnabled) { $backupParams = [ '--differential' => true, '--tag' => 'update-' . $jobId, @@ -941,7 +1010,7 @@ private function startUpdateJob(bool $fallback): void return; } } else { - $this->appendSupportEvent('update_skip_backup', 'Backup pre-update saltato', 'Modalita test/debug attivata da Supporto > Modifiche'); + $this->appendSupportEvent('update_skip_backup', 'Backup pre-update non eseguito', 'Procedura prevista ma sospesa su questo nodo finche il flusso operativo non viene consolidato.'); } $args = [ @@ -956,6 +1025,9 @@ private function startUpdateJob(bool $fallback): void if ($this->updateDryRun) { $args[] = '--dry-run'; } + if (! $backupEnabled) { + $args[] = '--skip-backup'; + } if ($this->updateForce || $fallback) { $args[] = '--force'; } @@ -981,9 +1053,9 @@ private function startUpdateJob(bool $fallback): void Notification::make() ->title('Aggiornamento avviato') - ->body($this->updateSkipBackup - ? 'Aggiornamento avviato senza backup pre-update (modalita test). Avanzamento visibile in tempo reale.' - : 'Backup pre-update completato. Avanzamento visibile in tempo reale.') + ->body($backupEnabled + ? 'Backup pre-update completato. Avanzamento visibile in tempo reale.' + : 'Aggiornamento avviato con backup solo indicato e non eseguito. Avanzamento visibile in tempo reale.') ->success() ->send(); } @@ -1012,6 +1084,10 @@ private function resolveAmministratoreId(): int private function shouldAttemptDrivePreupdateBackup(): bool { + if (! $this->shouldRunPreupdateBackup()) { + return false; + } + if (! (bool) config('distribution.preupdate_drive_enabled', false)) { return false; } @@ -1037,9 +1113,97 @@ private function shouldAttemptDrivePreupdateBackup(): bool private function shouldRequireDrivePreupdateBackup(): bool { + if (! $this->shouldRunPreupdateBackup()) { + return false; + } + return (bool) config('distribution.preupdate_require_drive', false); } + private function shouldRunPreupdateBackup(): bool + { + return (bool) config('distribution.preupdate_backup_enabled', false) && ! $this->updateSkipBackup; + } + + private function shouldUseDaemonUpdateExecutor(): bool + { + return trim((string) config('distribution.update_executor', 'local')) === 'daemon'; + } + + private function shouldShowPreupdateBackupPlan(): bool + { + return (bool) config('distribution.preupdate_backup_visible', true); + } + + private function queueDaemonNodeRefreshRequest(): void + { + $jobId = now()->format('YmdHis') . '-' . Str::lower(Str::random(6)); + $jobsDir = storage_path('app/support/update-jobs'); + $requestsDir = storage_path('app/support/update-requests'); + + if (! is_dir($jobsDir)) { + @mkdir($jobsDir, 0775, true); + } + + if (! is_dir($requestsDir)) { + @mkdir($requestsDir, 0775, true); + } + + $progressPath = $jobsDir . '/' . $jobId . '.progress.json'; + $requestPath = $requestsDir . '/' . $jobId . '.json'; + + $payload = [ + 'job_id' => $jobId, + 'requested_at' => now()->toIso8601String(), + 'requested_by' => optional(Auth::user())->email, + 'remote' => trim($this->gitRemote), + 'branch' => trim($this->gitBranch), + 'force' => (bool) $this->gitForce, + 'skip_backup' => ! $this->shouldRunPreupdateBackup(), + 'site_mode' => (string) config('netgescon.maintenance.site_mode', config('netgescon.site_mode', 'app')), + ]; + + $this->updateJobId = $jobId; + $this->updateInProgress = true; + $this->updateProgressPercent = 1; + $this->updateProgressMessage = 'Richiesta aggiornata accodata al daemon host...'; + $this->updateProgressStatus = 'queued'; + + @file_put_contents($progressPath, json_encode([ + 'timestamp' => now()->toIso8601String(), + 'percent' => 1, + 'message' => 'Richiesta accodata al daemon host Docker...', + 'status' => 'queued', + 'exit_code' => null, + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + + $written = @file_put_contents($requestPath, json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); + if ($written === false) { + $this->updateInProgress = false; + $this->updateProgressPercent = 100; + $this->updateProgressStatus = 'failed'; + $this->updateProgressMessage = 'Impossibile creare richiesta daemon'; + $this->lastUpdateExitCode = 1; + $this->lastUpdateIssue = 'Creazione richiesta daemon fallita.'; + + Notification::make() + ->title('Richiesta daemon fallita') + ->body('Il nodo non e riuscito a scrivere la richiesta verso il runner host.') + ->danger() + ->send(); + + return; + } + + $this->appendSupportEvent('update_daemon_request', 'Richiesta update accodata al daemon host', json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: $jobId); + + Notification::make() + ->title('Richiesta update accodata') + ->body('Il daemon host Docker puo eseguire ora il refresh completo senza accesso shell manuale.') + ->success() + ->send(); + } + private function registerPlannedUpdateSummary(): void { $summary = implode(' | ', $this->getUpdatePlannedStepsProperty()); @@ -1793,12 +1957,16 @@ private function startNodeRefreshJob(bool $skipMaintenance = false, bool $skipCo $this->updateJobId = $jobId; $this->updateInProgress = true; $this->updateProgressPercent = 1; + $backupEnabled = (bool) config('distribution.preupdate_backup_enabled', false) && ! $this->updateSkipBackup; + $this->updateProgressMessage = $skipComposer ? 'Preparazione refresh nodo da Gitea senza maintenance e senza composer...' : ($skipMaintenance ? 'Preparazione refresh nodo da Gitea senza maintenance mode...' - : 'Preparazione refresh nodo da Gitea...'); - $this->updateProgressStatus = 'running'; + : ($backupEnabled + ? 'Preparazione refresh nodo da Gitea...' + : 'Preparazione refresh nodo da Gitea con backup solo indicato...')); + $this->updateProgressStatus = 'running'; @file_put_contents($progressPath, json_encode([ 'timestamp' => now()->toIso8601String(), @@ -1807,7 +1975,9 @@ private function startNodeRefreshJob(bool $skipMaintenance = false, bool $skipCo ? 'Preparazione refresh nodo da Gitea senza maintenance e senza composer...' : ($skipMaintenance ? 'Preparazione refresh nodo da Gitea senza maintenance mode...' - : 'Preparazione refresh nodo da Gitea...'), + : ($backupEnabled + ? 'Preparazione refresh nodo da Gitea...' + : 'Preparazione refresh nodo da Gitea con backup solo indicato...')), 'status' => 'running', 'exit_code' => null, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); @@ -1829,7 +1999,7 @@ private function startNodeRefreshJob(bool $skipMaintenance = false, bool $skipCo $args[] = '--force'; } - if ($this->updateSkipBackup) { + if (! $backupEnabled) { $args[] = '--skip-backup'; } @@ -1873,10 +2043,10 @@ private function startNodeRefreshJob(bool $skipMaintenance = false, bool $skipCo Notification::make() ->title('Refresh nodo avviato') ->body($skipComposer - ? 'Refresh nodo avviato senza maintenance e senza composer. Usalo solo come recovery su macchine dove vendor non e scrivibile dal processo web.' - : ($skipMaintenance - ? 'Refresh nodo avviato senza maintenance mode. Usalo solo come percorso di riserva su macchine dove il blocco accessi non si attiva.' - : 'Sync da Gitea, migrate e rebuild cache verranno eseguiti direttamente sul nodo da browser.')) + ? 'Refresh nodo avviato senza maintenance e senza composer. Usalo solo come recovery su macchine dove vendor non e scrivibile dal processo web.' + : ($skipMaintenance + ? 'Refresh nodo avviato senza maintenance mode. Usalo solo come percorso di riserva su macchine dove il blocco accessi non si attiva.' + : 'Flusso autonomo avviato: backup, maintenance con pagina 503 personalizzata, sync da Gitea, rebuild applicativo e riapertura nodo.')) ->success() ->send(); } @@ -2143,6 +2313,34 @@ private function detectUpdateIssue(string $output, int $exitCode): ?string return 'Permessi insufficienti sul filesystem o su git durante update.'; } + if (str_contains($haystack, 'backup pre-update fallito')) { + return 'Backup pre-update non completato: l aggiornamento e stato fermato per sicurezza prima di modificare il nodo.'; + } + + if (str_contains($haystack, 'maintenance mode non attivata')) { + return 'La maintenance mode Laravel non si e attivata. Verifica che il nodo remoto stia eseguendo il codice aggiornato con il render 503 applicativo.'; + } + + if (str_contains($haystack, 'sync git fallita')) { + return 'La sync da Gitea e fallita. Controlla repository remoto, branch, working tree e permessi Git del nodo.'; + } + + if (str_contains($haystack, 'composer install fallito')) { + return 'Composer non e riuscito a riallineare le dipendenze sul nodo remoto. Verifica permessi, spazio disco e connettivita verso Packagist o mirror configurati.'; + } + + if (str_contains($haystack, 'migration fallita')) { + return 'Le migration non sono state applicate correttamente. Il nodo e stato riportato fuori maintenance, ma serve analizzare l output completo e gli errori runtime registrati.'; + } + + if (str_contains($haystack, 'view:clear fallito') || str_contains($haystack, 'view:cache fallita')) { + return 'La ricostruzione delle view Blade e fallita. Controlla cache, permessi di storage e output completo del refresh nodo.'; + } + + if (str_contains($haystack, 'nodo ancora in maintenance')) { + return 'Il refresh ha finito i passaggi applicativi ma il nodo non e stato riaperto automaticamente. Controlla il log e lo stato maintenance registrato.'; + } + return 'Aggiornamento terminato con errore tecnico. Controlla l\'output completo.'; } @@ -2198,7 +2396,12 @@ private function appendFromNdjson(string $file, string $sourceLabel, array &$eve return; } - $lines = @file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + $remaining = max(0, 250 - count($events)); + if ($remaining === 0) { + return; + } + + $lines = $this->readTailLines($file, $remaining, 1024 * 1024); if (! is_array($lines) || $lines === []) { return; } @@ -2229,13 +2432,17 @@ private function appendFromLaravelLog(string $file, array &$events): void return; } - $lines = @file($file, FILE_IGNORE_NEW_LINES); + $remaining = max(0, 450 - count($events)); + if ($remaining === 0) { + return; + } + + $lines = $this->readTailLines($file, 4000, 4 * 1024 * 1024); if (! is_array($lines) || $lines === []) { return; } - $slice = array_slice($lines, -4000); - foreach ($slice as $line) { + foreach ($lines as $line) { if (! is_string($line) || trim($line) === '') { continue; } @@ -2263,6 +2470,66 @@ private function appendFromLaravelLog(string $file, array &$events): void } } + /** @return array */ + private function readTailLines(string $file, int $lineLimit, int $byteLimit = 1048576): array + { + if ($lineLimit <= 0 || ! is_file($file)) { + return []; + } + + $handle = @fopen($file, 'rb'); + if ($handle === false) { + return []; + } + + $size = @filesize($file); + if (! is_int($size) || $size <= 0) { + fclose($handle); + + return []; + } + + $chunkSize = 65536; + $buffer = ''; + $position = $size; + $bytesRead = 0; + + while ($position > 0 && $bytesRead < $byteLimit) { + $readSize = min($chunkSize, $position, $byteLimit - $bytesRead); + if ($readSize <= 0) { + break; + } + + $position -= $readSize; + if (fseek($handle, $position) !== 0) { + break; + } + + $chunk = fread($handle, $readSize); + if (! is_string($chunk) || $chunk === '') { + break; + } + + $buffer = $chunk . $buffer; + $bytesRead += $readSize; + + if (substr_count($buffer, "\n") >= $lineLimit + 1) { + break; + } + } + + fclose($handle); + + $lines = preg_split('/\r\n|\n|\r/', $buffer); + if (! is_array($lines) || $lines === []) { + return []; + } + + $lines = array_values(array_filter($lines, static fn(mixed $line): bool => is_string($line) && trim($line) !== '')); + + return array_slice($lines, -$lineLimit); + } + /** * @param array $events * @return array diff --git a/docs/support/AGGIORNAMENTI-STAGING-GIT.md b/docs/support/AGGIORNAMENTI-STAGING-GIT.md index 27448dc..ac6f3da 100644 --- a/docs/support/AGGIORNAMENTI-STAGING-GIT.md +++ b/docs/support/AGGIORNAMENTI-STAGING-GIT.md @@ -117,6 +117,38 @@ ## Regola finale - comando tecnico sottostante: `netgescon:git-sync` - documentazione di cio che si sta distribuendo: file Markdown versionati nel repository +## Emergenza: pagina Supporto > Modifiche in out-of-memory + +Se un nodo remoto va in errore aprendo `admin-filament/supporto/aggiornamento-nodo` o `Supporto > Modifiche` con messaggi tipo `Allowed memory size exhausted`, la prima ipotesi da verificare e la dimensione dei log runtime. + +Controllo rapido consigliato: + +```bash +cd /var/www/netgescon +ls -lh storage/logs/laravel.log storage/logs/runtime-errors.ndjson storage/logs/support-events.ndjson +``` + +Segnale tipico: `laravel.log` molto grande e pagina che prova a leggere il file tutto in memoria. + +Rimedio corretto a regime: + +1. correggere il repository con lettura tail limitata dei log; +2. fare commit e push; +3. riallineare il nodo via `Supporto > Modifiche` o `php artisan netgescon:git-sync --remote=origin --branch=main`. + +Solo se il nodo e bloccato e serve rientrare subito prima del fix Git: + +```bash +cd /var/www/netgescon +php -l app/Filament/Pages/Supporto/Modifiche.php +php artisan optimize:clear +mv storage/logs/laravel.log storage/logs/laravel.log.$(date +%Y%m%d-%H%M%S).bak +touch storage/logs/laravel.log +chown www-data:www-data storage/logs/laravel.log +``` + +Nota operativa: questa rotazione manuale e solo una misura di contenimento. Il fix vero deve restare nel codice e poi essere riportato subito su Git, altrimenti il problema puo tornare al prossimo log grande o sul prossimo nodo remoto. + ## Rilascio operativo 2026-04-06 Questo e il blocco che puo essere aggiornato adesso con la procedura Git-first senza correzioni manuali extra su staging. @@ -176,3 +208,46 @@ ### Sync fornitori legacy 2026-04-08 - non correggere staging a mano se il problema e gia nel repository - prima si corregge Day0, poi commit, poi push, poi update UI da Gitea - se un fix urgente viene fatto su staging per emergenza, va riportato subito nel repository prima del prossimo aggiornamento + +## Stato operativo 2026-05-09 + +### Nodi client demo/prod + +Per i nodi live che controllano gli aggiornamenti da browser o da `php artisan netgescon:update`, e stato verificato che il DNS pubblico di `updates.netgescon.it` non basta: il client va in timeout verso `195.110.133.76:443`. + +Allineamento corretto, gia applicato ai container demo/prod: + +```env +NETGESCON_UPDATE_RESOLVE=updates.netgescon.it:443:192.168.0.53 +``` + +Questa impostazione va considerata parte del runbook operativo finche il DNS interno non viene risolto alla radice. + +### Server update interno `.53` + +Il problema residuo non e piu sul nodo client ma sulla macchina `192.168.0.53` che ospita `updates.netgescon.it`. + +Verifiche eseguite: + +- `GET /api/v1/distribution/health` risponde `200 OK`; +- `GET /api/v1/distribution/updates/manifest?channel=free` risponde `404 Not Found`; +- nel checkout attuale della `.53` le route pubbliche `updates/manifest` e `updates/package` non risultano deployate; +- in `storage/app/distribution` non risultano manifest pubblicati. + +Conclusione pratica: + +- il client ora punta correttamente alla `.53`; +- il server update `.53` non e ancora pronto a servire i manifest. + +### Cosa chiedere all agent sulla `.53` + +L altro agent sulla `.53` deve lavorare su questo elenco minimo: + +1. confrontare `routes/api.php` del checkout `.53` con Day0 e inserire le route pubbliche mancanti del blocco distribution; +2. confrontare `app/Http/Controllers/Api/DistributionController.php` e portare la versione che include `updatesManifest()` e `updatesPackage()`; +3. verificare la presenza di `config/distribution.php` e del motore update coerente con Day0; +4. pubblicare `storage/app/distribution/free/manifest.json` e il relativo zip; +5. eseguire `php artisan optimize:clear`; +6. validare con `curl` locale verso `health` e `updates/manifest`. + +Se il checkout `.53` resta sporco, evitare sync distruttivi. In quel caso fare merge selettivo solo sui file del motore update oppure preparare un checkout pulito dedicato al server update. diff --git a/docs/support/BUG-RISOLTI-E-VALIDAZIONI.md b/docs/support/BUG-RISOLTI-E-VALIDAZIONI.md index dfabf18..6c6f600 100644 --- a/docs/support/BUG-RISOLTI-E-VALIDAZIONI.md +++ b/docs/support/BUG-RISOLTI-E-VALIDAZIONI.md @@ -11,6 +11,14 @@ ## Obiettivo ## Registro sintetico +### Supporto > Modifiche in memory exhaustion su log grandi + +- Problema: la pagina `admin-filament/supporto/aggiornamento-nodo` poteva andare in `Allowed memory size exhausted` durante `loadRuntimeErrors()` quando `storage/logs/laravel.log` diventava molto grande. Sul nodo staging il caso reale era un `laravel.log` da circa `278 MB`. +- Causa tecnica: `appendFromNdjson()` e `appendFromLaravelLog()` usavano `file(...)`, quindi caricavano l intero file in RAM anche se poi la UI mostrava solo gli ultimi eventi utili. +- Fix applicato: in `app/Filament/Pages/Supporto/Modifiche.php` i lettori sono stati sostituiti con una lettura tail limitata per linee e byte (`readTailLines()`), mantenendo il comportamento della UI ma senza allocare l intero log. +- Stato: [U] riprodotto e corretto sul nodo locale `/var/www/netgescon`; validazione eseguita via reflection su `loadRuntimeErrors()` con `laravel.log` grande, picco memoria rimasto intorno a `89 MB` e `39` righe BUG caricate correttamente. +- Prevenzione: non usare aumento del `memory_limit` come rimedio principale; mantenere logrotate attivo e preferire sempre parsing incrementale o tail bounded per i file operativi letti dalla UI. + ### Ripristino bootstrap pannello admin - Problema: `AdminFilamentPanelProvider.php` era stato corrotto da una modifica incompleta e impediva il bootstrap corretto. @@ -46,6 +54,7 @@ ## Validazioni eseguite nell ultimo ciclo - `php artisan list` eseguito con esito positivo dopo gli ultimi fix applicativi - controlli sintattici PHP sulle classi modificate senza errori - diagnostica editor senza errori sui file toccati +- `Supporto > Modifiche`: validato il nuovo parsing tail-bounded dei log su nodo con `laravel.log` da `278 MB`, senza esaurimento memoria ## Validazioni ancora aperte