Fix support update log memory exhaustion

This commit is contained in:
michele 2026-05-24 21:07:24 +00:00
parent c4f8c827b4
commit cd2eed25bc
3 changed files with 387 additions and 36 deletions

View File

@ -5,6 +5,8 @@
use App\Models\Stabile; use App\Models\Stabile;
use App\Models\SupportBugRegistry; use App\Models\SupportBugRegistry;
use App\Models\SupportUpdateEntry; use App\Models\SupportUpdateEntry;
use App\Models\User;
use App\Services\ProgramAclService;
use App\Support\StabileContext; use App\Support\StabileContext;
use BackedEnum; use BackedEnum;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
@ -228,20 +230,23 @@ class Modifiche extends Page
/** @var array<int,array{key:string,title:string,description:string,path:string,exists:bool,html:string}> */ /** @var array<int,array{key:string,title:string,description:string,path:string,exists:bool,html:string}> */
public array $manualSections = []; public array $manualSections = [];
/** @var array<int,array<string,mixed>> */
public array $invoiceExtractionProfiles = [];
public function mount(): void public function mount(): void
{ {
$this->updateSkipBackup = ! $this->shouldRunPreupdateBackup();
$this->reloadDashboardData(); $this->reloadDashboardData();
} }
public static function canAccess(): bool public static function canAccess(): bool
{ {
$user = Auth::user(); $user = Auth::user();
if (! $user) { if (! $user instanceof User) {
return false; return false;
} }
// Pagina informativa: accessibile a chi accede al pannello. return app(ProgramAclService::class)->canAccessProgram($user, 'supporto.modifiche');
return true;
} }
public function canRunUpdate(): bool public function canRunUpdate(): bool
@ -258,6 +263,7 @@ public function reloadDashboardData(): void
$this->loadVersionInfo(); $this->loadVersionInfo();
$this->loadLatestCommits(); $this->loadLatestCommits();
$this->loadCommitNotes(); $this->loadCommitNotes();
$this->loadInvoiceExtractionProfiles();
$this->loadRecentFilamentPages(); $this->loadRecentFilamentPages();
$this->loadFunctionalHighlights(); $this->loadFunctionalHighlights();
$this->loadManualSections(); $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 public function runUpdate(): void
{ {
if (! $this->canRunUpdate()) { if (! $this->canRunUpdate()) {
@ -329,6 +357,11 @@ public function runGitSync(): void
} }
public function runNodeRefresh(): void public function runNodeRefresh(): void
{
$this->runAutonomousNodeRefresh();
}
public function runAutonomousNodeRefresh(): void
{ {
if (! $this->canRunUpdate()) { if (! $this->canRunUpdate()) {
Notification::make() Notification::make()
@ -340,6 +373,22 @@ public function runNodeRefresh(): void
return; 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(); $this->startNodeRefreshJob();
} }
@ -435,6 +484,20 @@ public function refreshUpdateProgress(): void
if ($exitCode !== 0) { if ($exitCode !== 0) {
$this->appendSupportEvent('update_async', 'Aggiornamento asincrono fallito', (string) ($this->lastUpdateOutput ?? '')); $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(); $this->reloadDashboardData();
@ -578,21 +641,25 @@ public function runMaintenanceMigrate(): void
*/ */
public function getUpdatePlannedStepsProperty(): array public function getUpdatePlannedStepsProperty(): array
{ {
$requireDrive = $this->shouldRequireDrivePreupdateBackup(); $backupEnabled = $this->shouldRunPreupdateBackup();
$driveEnabled = $this->shouldAttemptDrivePreupdateBackup(); $backupVisible = $this->shouldShowPreupdateBackupPlan();
$requireDrive = $this->shouldRequireDrivePreupdateBackup();
$driveEnabled = $this->shouldAttemptDrivePreupdateBackup();
$steps = [ $steps = [
'Verifica canale update: ' . $this->updateChannel, 'Verifica canale update: ' . $this->updateChannel,
$this->updateSkipBackup $backupEnabled
? 'Backup pre-update saltato manualmente (modalita test/debug)' ? 'Backup pre-update automatico (snapshot differenziale + indice record)'
: 'Backup pre-update automatico (snapshot differenziale + indice record)', : ($backupVisible
$this->updateSkipBackup ? 'Backup pre-update previsto ma temporaneamente disattivato finche la procedura non e definitiva'
? 'Upload backup su Google Drive non eseguito perche il backup e stato saltato' : 'Backup pre-update non previsto per questo nodo'),
: ($requireDrive $backupEnabled
? ($requireDrive
? 'Upload backup su Google Drive obbligatorio prima dell update' ? 'Upload backup su Google Drive obbligatorio prima dell update'
: ($driveEnabled : ($driveEnabled
? 'Upload backup su Google Drive tentato se configurato sul sito corrente' ? '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)'), 'Esecuzione in modalita ' . ($this->updateDryRun ? 'dry-run (nessuna scrittura)' : 'apply (aggiornamento reale)'),
'Flag force: ' . ($this->updateForce ? 'abilitato' : 'disabilitato'), 'Flag force: ' . ($this->updateForce ? 'abilitato' : 'disabilitato'),
'Check endpoint update consigliato prima del lancio', 'Check endpoint update consigliato prima del lancio',
@ -832,6 +899,8 @@ private function startUpdateJob(bool $fallback): void
{ {
$this->registerPlannedUpdateSummary(); $this->registerPlannedUpdateSummary();
$backupEnabled = $this->shouldRunPreupdateBackup();
$jobId = now()->format('YmdHis') . '-' . Str::lower(Str::random(6)); $jobId = now()->format('YmdHis') . '-' . Str::lower(Str::random(6));
$jobsDir = storage_path('app/support/update-jobs'); $jobsDir = storage_path('app/support/update-jobs');
if (! is_dir($jobsDir)) { if (! is_dir($jobsDir)) {
@ -844,17 +913,17 @@ private function startUpdateJob(bool $fallback): void
$this->updateJobId = $jobId; $this->updateJobId = $jobId;
$this->updateInProgress = true; $this->updateInProgress = true;
$this->updateProgressPercent = 1; $this->updateProgressPercent = 1;
$this->updateProgressMessage = $this->updateSkipBackup $this->updateProgressMessage = $backupEnabled
? 'Preparazione aggiornamento senza backup (modalita test)...' ? 'Preparazione backup pre-update...'
: 'Preparazione backup pre-update...'; : 'Preparazione aggiornamento con backup solo indicato...';
$this->updateProgressStatus = 'running'; $this->updateProgressStatus = 'running';
@file_put_contents($progressPath, json_encode([ @file_put_contents($progressPath, json_encode([
'timestamp' => now()->toIso8601String(), 'timestamp' => now()->toIso8601String(),
'percent' => 1, 'percent' => 1,
'message' => $this->updateSkipBackup 'message' => $backupEnabled
? 'Preparazione aggiornamento senza backup (modalita test)...' ? 'Preparazione backup pre-update...'
: 'Preparazione backup pre-update...', : 'Preparazione aggiornamento con backup solo indicato...',
'status' => 'running', 'status' => 'running',
'exit_code' => null, 'exit_code' => null,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
@ -863,7 +932,7 @@ private function startUpdateJob(bool $fallback): void
$requireDrive = $this->shouldRequireDrivePreupdateBackup(); $requireDrive = $this->shouldRequireDrivePreupdateBackup();
$driveEnabled = $this->shouldAttemptDrivePreupdateBackup(); $driveEnabled = $this->shouldAttemptDrivePreupdateBackup();
if ($this->updateSkipBackup) { if (! $backupEnabled) {
$requireDrive = false; $requireDrive = false;
$driveEnabled = false; $driveEnabled = false;
} }
@ -886,7 +955,7 @@ private function startUpdateJob(bool $fallback): void
return; return;
} }
if (! $this->updateSkipBackup) { if ($backupEnabled) {
$backupParams = [ $backupParams = [
'--differential' => true, '--differential' => true,
'--tag' => 'update-' . $jobId, '--tag' => 'update-' . $jobId,
@ -941,7 +1010,7 @@ private function startUpdateJob(bool $fallback): void
return; return;
} }
} else { } 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 = [ $args = [
@ -956,6 +1025,9 @@ private function startUpdateJob(bool $fallback): void
if ($this->updateDryRun) { if ($this->updateDryRun) {
$args[] = '--dry-run'; $args[] = '--dry-run';
} }
if (! $backupEnabled) {
$args[] = '--skip-backup';
}
if ($this->updateForce || $fallback) { if ($this->updateForce || $fallback) {
$args[] = '--force'; $args[] = '--force';
} }
@ -981,9 +1053,9 @@ private function startUpdateJob(bool $fallback): void
Notification::make() Notification::make()
->title('Aggiornamento avviato') ->title('Aggiornamento avviato')
->body($this->updateSkipBackup ->body($backupEnabled
? 'Aggiornamento avviato senza backup pre-update (modalita test). Avanzamento visibile in tempo reale.' ? 'Backup pre-update completato. Avanzamento visibile in tempo reale.'
: 'Backup pre-update completato. Avanzamento visibile in tempo reale.') : 'Aggiornamento avviato con backup solo indicato e non eseguito. Avanzamento visibile in tempo reale.')
->success() ->success()
->send(); ->send();
} }
@ -1012,6 +1084,10 @@ private function resolveAmministratoreId(): int
private function shouldAttemptDrivePreupdateBackup(): bool private function shouldAttemptDrivePreupdateBackup(): bool
{ {
if (! $this->shouldRunPreupdateBackup()) {
return false;
}
if (! (bool) config('distribution.preupdate_drive_enabled', false)) { if (! (bool) config('distribution.preupdate_drive_enabled', false)) {
return false; return false;
} }
@ -1037,9 +1113,97 @@ private function shouldAttemptDrivePreupdateBackup(): bool
private function shouldRequireDrivePreupdateBackup(): bool private function shouldRequireDrivePreupdateBackup(): bool
{ {
if (! $this->shouldRunPreupdateBackup()) {
return false;
}
return (bool) config('distribution.preupdate_require_drive', 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 private function registerPlannedUpdateSummary(): void
{ {
$summary = implode(' | ', $this->getUpdatePlannedStepsProperty()); $summary = implode(' | ', $this->getUpdatePlannedStepsProperty());
@ -1793,12 +1957,16 @@ private function startNodeRefreshJob(bool $skipMaintenance = false, bool $skipCo
$this->updateJobId = $jobId; $this->updateJobId = $jobId;
$this->updateInProgress = true; $this->updateInProgress = true;
$this->updateProgressPercent = 1; $this->updateProgressPercent = 1;
$backupEnabled = (bool) config('distribution.preupdate_backup_enabled', false) && ! $this->updateSkipBackup;
$this->updateProgressMessage = $skipComposer $this->updateProgressMessage = $skipComposer
? 'Preparazione refresh nodo da Gitea senza maintenance e senza composer...' ? 'Preparazione refresh nodo da Gitea senza maintenance e senza composer...'
: ($skipMaintenance : ($skipMaintenance
? 'Preparazione refresh nodo da Gitea senza maintenance mode...' ? 'Preparazione refresh nodo da Gitea senza maintenance mode...'
: 'Preparazione refresh nodo da Gitea...'); : ($backupEnabled
$this->updateProgressStatus = 'running'; ? 'Preparazione refresh nodo da Gitea...'
: 'Preparazione refresh nodo da Gitea con backup solo indicato...'));
$this->updateProgressStatus = 'running';
@file_put_contents($progressPath, json_encode([ @file_put_contents($progressPath, json_encode([
'timestamp' => now()->toIso8601String(), '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...' ? 'Preparazione refresh nodo da Gitea senza maintenance e senza composer...'
: ($skipMaintenance : ($skipMaintenance
? 'Preparazione refresh nodo da Gitea senza maintenance mode...' ? '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', 'status' => 'running',
'exit_code' => null, 'exit_code' => null,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
@ -1829,7 +1999,7 @@ private function startNodeRefreshJob(bool $skipMaintenance = false, bool $skipCo
$args[] = '--force'; $args[] = '--force';
} }
if ($this->updateSkipBackup) { if (! $backupEnabled) {
$args[] = '--skip-backup'; $args[] = '--skip-backup';
} }
@ -1873,10 +2043,10 @@ private function startNodeRefreshJob(bool $skipMaintenance = false, bool $skipCo
Notification::make() Notification::make()
->title('Refresh nodo avviato') ->title('Refresh nodo avviato')
->body($skipComposer ->body($skipComposer
? 'Refresh nodo avviato senza maintenance e senza composer. Usalo solo come recovery su macchine dove vendor non e scrivibile dal processo web.' ? 'Refresh nodo avviato senza maintenance e senza composer. Usalo solo come recovery su macchine dove vendor non e scrivibile dal processo web.'
: ($skipMaintenance : ($skipMaintenance
? 'Refresh nodo avviato senza maintenance mode. Usalo solo come percorso di riserva su macchine dove il blocco accessi non si attiva.' ? '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.')) : 'Flusso autonomo avviato: backup, maintenance con pagina 503 personalizzata, sync da Gitea, rebuild applicativo e riapertura nodo.'))
->success() ->success()
->send(); ->send();
} }
@ -2143,6 +2313,34 @@ private function detectUpdateIssue(string $output, int $exitCode): ?string
return 'Permessi insufficienti sul filesystem o su git durante update.'; 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.'; return 'Aggiornamento terminato con errore tecnico. Controlla l\'output completo.';
} }
@ -2198,7 +2396,12 @@ private function appendFromNdjson(string $file, string $sourceLabel, array &$eve
return; 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 === []) { if (! is_array($lines) || $lines === []) {
return; return;
} }
@ -2229,13 +2432,17 @@ private function appendFromLaravelLog(string $file, array &$events): void
return; 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 === []) { if (! is_array($lines) || $lines === []) {
return; return;
} }
$slice = array_slice($lines, -4000); foreach ($lines as $line) {
foreach ($slice as $line) {
if (! is_string($line) || trim($line) === '') { if (! is_string($line) || trim($line) === '') {
continue; continue;
} }
@ -2263,6 +2470,66 @@ private function appendFromLaravelLog(string $file, array &$events): void
} }
} }
/** @return array<int, string> */
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<int,array{timestamp:string,type:string,message:string,context:string,source:string}> $events * @param array<int,array{timestamp:string,type:string,message:string,context:string,source:string}> $events
* @return array<int,array{timestamp:string,type:string,message:string,context:string,source:string,fingerprint:string,bug_id:int,bug_code:string,bug_status:string,occurrences:int,resolution_note:string,resolved_at:?string}> * @return array<int,array{timestamp:string,type:string,message:string,context:string,source:string,fingerprint:string,bug_id:int,bug_code:string,bug_status:string,occurrences:int,resolution_note:string,resolved_at:?string}>

View File

@ -117,6 +117,38 @@ ## Regola finale
- comando tecnico sottostante: `netgescon:git-sync` - comando tecnico sottostante: `netgescon:git-sync`
- documentazione di cio che si sta distribuendo: file Markdown versionati nel repository - 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 ## 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. 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 - 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 - 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 - 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.

View File

@ -11,6 +11,14 @@ ## Obiettivo
## Registro sintetico ## 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 ### Ripristino bootstrap pannello admin
- Problema: `AdminFilamentPanelProvider.php` era stato corrotto da una modifica incompleta e impediva il bootstrap corretto. - 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 - `php artisan list` eseguito con esito positivo dopo gli ultimi fix applicativi
- controlli sintattici PHP sulle classi modificate senza errori - controlli sintattici PHP sulle classi modificate senza errori
- diagnostica editor senza errori sui file toccati - 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 ## Validazioni ancora aperte