diff --git a/app/Console/Commands/NetgesconNodeRefreshCommand.php b/app/Console/Commands/NetgesconNodeRefreshCommand.php new file mode 100644 index 0000000..76eb95d --- /dev/null +++ b/app/Console/Commands/NetgesconNodeRefreshCommand.php @@ -0,0 +1,141 @@ +option('remote')) ?: 'origin'; + $branch = trim((string) $this->option('branch')) ?: 'main'; + $force = (bool) $this->option('force'); + $skipMigrate = (bool) $this->option('skip-migrate'); + $skipViewCache = (bool) $this->option('skip-view-cache'); + + $this->writeProgress(2, 'Avvio refresh nodo da Gitea', 'running'); + + $gitParams = [ + '--remote' => $remote, + '--branch' => $branch, + ]; + + if ($force) { + $gitParams['--force'] = true; + } + + $this->info("Sync Git da {$remote}/{$branch}..."); + $this->writeProgress(15, 'Sync Git da Gitea in corso', 'running'); + $gitExit = Artisan::call('netgescon:git-sync', $gitParams); + $gitOutput = trim((string) Artisan::output()); + + if ($gitExit !== 0) { + $this->error('Sync Git fallita.'); + $this->line($gitOutput); + $this->writeProgress(100, 'Sync Git fallita', 'failed', $gitExit); + + return self::FAILURE; + } + + $fullOutput = [ + 'NODE REFRESH: netgescon:git-sync', + $gitOutput, + ]; + + if (! $skipMigrate) { + $this->info('Esecuzione migrate --force...'); + $this->writeProgress(55, 'Applicazione migration', 'running'); + $migrateExit = Artisan::call('migrate', ['--force' => true]); + $migrateOutput = trim((string) Artisan::output()); + $fullOutput[] = 'NODE REFRESH: php artisan migrate --force'; + $fullOutput[] = $migrateOutput; + + if ($migrateExit !== 0) { + $this->error('Migration fallita.'); + $this->line($migrateOutput); + $this->writeProgress(100, 'Migration fallita', 'failed', $migrateExit); + + return self::FAILURE; + } + } + + $this->info('Pulizia cache applicative...'); + $this->writeProgress(72, 'Pulizia cache applicative', 'running'); + $optimizeExit = Artisan::call('optimize:clear'); + $optimizeOutput = trim((string) Artisan::output()); + $fullOutput[] = 'NODE REFRESH: php artisan optimize:clear'; + $fullOutput[] = $optimizeOutput; + + if ($optimizeExit !== 0) { + $this->error('optimize:clear fallito.'); + $this->line($optimizeOutput); + $this->writeProgress(100, 'Pulizia cache fallita', 'failed', $optimizeExit); + + return self::FAILURE; + } + + if (! $skipViewCache) { + $this->info('Ricostruzione view cache...'); + $this->writeProgress(88, 'Ricostruzione view cache', 'running'); + + $viewClearExit = Artisan::call('view:clear'); + $viewClearOutput = trim((string) Artisan::output()); + $fullOutput[] = 'NODE REFRESH: php artisan view:clear'; + $fullOutput[] = $viewClearOutput; + + if ($viewClearExit !== 0) { + $this->error('view:clear fallito.'); + $this->line($viewClearOutput); + $this->writeProgress(100, 'view:clear fallito', 'failed', $viewClearExit); + + return self::FAILURE; + } + + $viewCacheExit = Artisan::call('view:cache'); + $viewCacheOutput = trim((string) Artisan::output()); + $fullOutput[] = 'NODE REFRESH: php artisan view:cache'; + $fullOutput[] = $viewCacheOutput; + + if ($viewCacheExit !== 0) { + $this->error('view:cache fallita.'); + $this->line($viewCacheOutput); + $this->writeProgress(100, 'view:cache fallita', 'failed', $viewCacheExit); + + return self::FAILURE; + } + } + + $this->line(implode(PHP_EOL . PHP_EOL, array_filter($fullOutput, static fn(string $chunk): bool => trim($chunk) !== ''))); + $this->writeProgress(100, 'Refresh nodo completato', 'completed', 0); + + return self::SUCCESS; + } + + private function writeProgress(int $percent, string $message, string $status, ?int $exitCode = null): void + { + $path = trim((string) $this->option('progress-file')); + if ($path === '') { + return; + } + + @file_put_contents($path, json_encode([ + 'timestamp' => now()->toIso8601String(), + 'percent' => max(0, min(100, $percent)), + 'message' => $message, + 'status' => $status, + 'exit_code' => $exitCode, + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + } +} \ No newline at end of file diff --git a/app/Filament/Pages/Supporto/Modifiche.php b/app/Filament/Pages/Supporto/Modifiche.php index 0867aba..77d0b21 100644 --- a/app/Filament/Pages/Supporto/Modifiche.php +++ b/app/Filament/Pages/Supporto/Modifiche.php @@ -223,6 +223,8 @@ class Modifiche extends Page /** @var array|null */ public ?array $lastNotificationSanitization = null; + public bool $ticketAttachmentMetadataReady = false; + /** @var array */ public array $manualSections = []; @@ -266,6 +268,7 @@ public function reloadDashboardData(): void $this->loadRuntimeErrors(); $this->loadLatestBackupSummary(); $this->loadLastNotificationSanitization(); + $this->loadAttachmentMetadataStatus(); if ($this->supportUpdateFormRecordedAt === '') { $this->resetSupportUpdateForm(); @@ -325,6 +328,21 @@ public function runGitSync(): void $this->startGitSyncJob(); } + public function runNodeRefresh(): void + { + if (! $this->canRunUpdate()) { + Notification::make() + ->title('Permessi insufficienti') + ->body('Solo super-admin/admin/amministratore possono lanciare il refresh completo del nodo.') + ->danger() + ->send(); + + return; + } + + $this->startNodeRefreshJob(); + } + public function generateDevelopmentSnapshot(): void { if (! $this->canRunUpdate()) { @@ -500,6 +518,31 @@ public function runMaintenanceViewRebuild(): void ->send(); } + public function runMaintenanceMigrate(): void + { + if (! $this->canRunUpdate()) { + Notification::make()->title('Permessi insufficienti')->danger()->send(); + return; + } + + $result = Process::path(base_path())->timeout(600)->run(['php', 'artisan', 'migrate', '--force']); + $out = trim((string) ($result->output() !== '' ? $result->output() : $result->errorOutput())); + + $this->lastUpdateAt = now()->format('d/m/Y H:i:s'); + $this->lastUpdateExitCode = $result->exitCode(); + $this->lastUpdateOutput = 'MANUTENZIONE: migrate --force' . PHP_EOL . $out; + $this->lastUpdateIssue = $result->successful() ? null : 'Errore durante migrate --force.'; + + if (! $result->successful()) { + $this->appendSupportEvent('maintenance', 'migrate fallita', $out); + } + + Notification::make() + ->title($result->successful() ? 'Migration applicate' : 'Errore migrate') + ->{$result->successful() ? 'success' : 'danger'}() + ->send(); + } + /** * @return array */ @@ -1224,6 +1267,16 @@ private function loadLastNotificationSanitization(): void $this->lastNotificationSanitization = $decoded; } + private function loadAttachmentMetadataStatus(): void + { + try { + $this->ticketAttachmentMetadataReady = Schema::hasTable('ticket_attachments') + && Schema::hasColumn('ticket_attachments', 'metadata'); + } catch (Throwable) { + $this->ticketAttachmentMetadataReady = false; + } + } + private function loadDevelopmentSnapshot(): void { $generatedPath = storage_path('app/support/generated/CONTINUITA-SVILUPPO-SNAPSHOT.md'); @@ -1696,6 +1749,71 @@ private function startGitSyncJob(): void ->send(); } + private function startNodeRefreshJob(): void + { + $jobId = now()->format('YmdHis') . '-' . Str::lower(Str::random(6)); + $jobsDir = storage_path('app/support/update-jobs'); + if (! is_dir($jobsDir)) { + @mkdir($jobsDir, 0775, true); + } + + $progressPath = $jobsDir . '/' . $jobId . '.progress.json'; + $logPath = $jobsDir . '/' . $jobId . '.log'; + + $this->updateJobId = $jobId; + $this->updateInProgress = true; + $this->updateProgressPercent = 1; + $this->updateProgressMessage = 'Preparazione refresh nodo da Gitea...'; + $this->updateProgressStatus = 'running'; + + @file_put_contents($progressPath, json_encode([ + 'timestamp' => now()->toIso8601String(), + 'percent' => 1, + 'message' => 'Preparazione refresh nodo da Gitea...', + 'status' => 'running', + 'exit_code' => null, + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + + $args = [ + 'php', + 'artisan', + 'netgescon:node-refresh', + '--remote=' . trim($this->gitRemote), + '--branch=' . trim($this->gitBranch), + '--progress-file=' . $progressPath, + ]; + + if ($this->gitForce) { + $args[] = '--force'; + } + + $escaped = implode(' ', array_map(static fn(string $arg): string => escapeshellarg($arg), $args)); + $shell = 'nohup ' . $escaped . ' > ' . escapeshellarg($logPath) . ' 2>&1 & echo $!'; + $started = Process::path(base_path())->run(['bash', '-lc', $shell]); + + if (! $started->successful()) { + $this->updateInProgress = false; + $this->updateProgressStatus = 'failed'; + $this->updateProgressPercent = 100; + $this->updateProgressMessage = 'Impossibile avviare refresh nodo'; + $this->lastUpdateExitCode = 1; + $this->lastUpdateOutput = trim((string) ($started->output() !== '' ? $started->output() : $started->errorOutput())); + $this->lastUpdateIssue = 'Avvio refresh nodo fallito.'; + + return; + } + + $this->updateProgressPercent = 5; + $this->updateProgressMessage = 'Job refresh nodo avviato (PID ' . trim($started->output()) . ')'; + $this->updateProgressStatus = 'running'; + + Notification::make() + ->title('Refresh nodo avviato') + ->body('Sync da Gitea, migrate e rebuild cache verranno eseguiti direttamente sul nodo da browser.') + ->success() + ->send(); + } + private function loadManualSections(): void { $this->manualSections = []; diff --git a/resources/views/filament/pages/supporto/aggiornamento-launcher.blade.php b/resources/views/filament/pages/supporto/aggiornamento-launcher.blade.php index 43ac542..2f549f9 100644 --- a/resources/views/filament/pages/supporto/aggiornamento-launcher.blade.php +++ b/resources/views/filament/pages/supporto/aggiornamento-launcher.blade.php @@ -34,6 +34,12 @@
Sync Git da Gitea
Flusso consigliato per staging e nodi interni.
+ @if(! $this->ticketAttachmentMetadataReady) +
+ Il nodo non ha ancora la colonna ticket_attachments.metadata. Finche non applichi le migration da questa pagina, GPS ed EXIF dei ticket non possono essere persistiti correttamente. +
+ @endif +
+ + Aggiorna nodo completo da Gitea + Aggiorna staging da Gitea @@ -99,7 +108,7 @@
- Git sync e update codice aggiornano i file versionati nel repository. Gli archivi caricati manualmente dentro storage o cartelle private non vengono trasferiti da Git, salvo procedure dedicate di backup/sync dati. + Il flusso consigliato del nodo e: push su Gitea, poi "Aggiorna nodo completo da Gitea" da questa pagina. Gli archivi caricati manualmente dentro storage o cartelle private non vengono trasferiti da Git, salvo procedure dedicate di backup/sync dati.
@if(filled($this->pendingGitPreviewIssue)) @@ -138,6 +147,9 @@
+ + Applica migration + Lancia aggiornamento