From 8ee59acb31689f304c900931ea232e521fd1c18b Mon Sep 17 00:00:00 2001 From: michele Date: Sat, 28 Mar 2026 17:29:40 +0000 Subject: [PATCH] Supporto Modifiche: aggiunge sync staging da Gitea --- .../NetgesconDevelopmentSnapshotCommand.php | 295 +++++++++++ .../Commands/NetgesconGitSyncCommand.php | 262 ++++++++++ app/Filament/Pages/Supporto/Modifiche.php | 481 ++++++++++++++++-- bootstrap/app.php | 6 + .../pages/supporto/modifiche.blade.php | 238 ++++++++- 5 files changed, 1220 insertions(+), 62 deletions(-) create mode 100644 app/Console/Commands/NetgesconDevelopmentSnapshotCommand.php create mode 100644 app/Console/Commands/NetgesconGitSyncCommand.php diff --git a/app/Console/Commands/NetgesconDevelopmentSnapshotCommand.php b/app/Console/Commands/NetgesconDevelopmentSnapshotCommand.php new file mode 100644 index 0000000..7a31acb --- /dev/null +++ b/app/Console/Commands/NetgesconDevelopmentSnapshotCommand.php @@ -0,0 +1,295 @@ + Modifiche'; + + public function handle(): int + { + $generatedDir = storage_path('app/support/generated'); + File::ensureDirectoryExists($generatedDir); + + $outputPath = trim((string) $this->option('output')) !== '' + ? trim((string) $this->option('output')) + : $generatedDir . '/CONTINUITA-SVILUPPO-SNAPSHOT.md'; + + $jsonPath = trim((string) $this->option('json')) !== '' + ? trim((string) $this->option('json')) + : $generatedDir . '/CONTINUITA-SVILUPPO-SNAPSHOT.json'; + + $manualPath = base_path('docs/support/MANUALE-SVILUPPO-OPERATIVO.md'); + $bugPath = base_path('docs/support/BUG-RISOLTI-E-VALIDAZIONI.md'); + $continuityPath = base_path('docs/support/CONTINUITA-SVILUPPO-AGENT.md'); + $pbxPath = base_path('docs/support/PBX-PANASONIC-STATO-OPERATIVO.md'); + + $manual = is_file($manualPath) ? (string) @file_get_contents($manualPath) : ''; + $bugs = is_file($bugPath) ? (string) @file_get_contents($bugPath) : ''; + $continuity = is_file($continuityPath) ? (string) @file_get_contents($continuityPath) : ''; + $pbx = is_file($pbxPath) ? (string) @file_get_contents($pbxPath) : ''; + + $branch = $this->gitValue(['rev-parse', '--abbrev-ref', 'HEAD']) ?? '-'; + $commit = $this->gitValue(['rev-parse', '--short', 'HEAD']) ?? '-'; + + $recentCommits = $this->recentCommits(); + $recentPages = $this->recentPages(); + $commitNotes = $this->recentCommitNotes(); + + $snapshot = []; + $snapshot[] = '# Snapshot continuita sviluppo'; + $snapshot[] = ''; + $snapshot[] = 'Generata automaticamente il ' . now()->format('d/m/Y H:i:s') . '.'; + $snapshot[] = ''; + $snapshot[] = '## Stato Git corrente'; + $snapshot[] = ''; + $snapshot[] = '- Branch: `' . $branch . '`'; + $snapshot[] = '- Commit: `' . $commit . '`'; + $snapshot[] = '- Workspace: `' . base_path() . '`'; + $snapshot[] = ''; + $snapshot[] = '## Focus operativo'; + $snapshot[] = ''; + $snapshot[] = $this->normalizeSectionLines($this->extractSection($manual, '## Implementazioni recenti da tenere come riferimento')); + $snapshot[] = ''; + $snapshot[] = '## Stato PBX Panasonic'; + $snapshot[] = ''; + $snapshot[] = $this->normalizeSectionLines($this->extractSection($pbx, '## Stato attuale')); + $snapshot[] = ''; + $snapshot[] = '## Validazioni ancora aperte'; + $snapshot[] = ''; + $snapshot[] = $this->normalizeSectionLines($this->extractSection($bugs, '## Validazioni ancora aperte')); + $snapshot[] = ''; + $snapshot[] = '## Prossimi passi consigliati'; + $snapshot[] = ''; + $snapshot[] = $this->normalizeSectionLines($this->extractSection($manual, '## Prossimi passi consigliati')); + $snapshot[] = ''; + $snapshot[] = '## Regole di continuita'; + $snapshot[] = ''; + $snapshot[] = $this->normalizeSectionLines($this->extractSection($continuity, '## Regola semplice')); + $snapshot[] = ''; + $snapshot[] = '## Ultimi commit'; + $snapshot[] = ''; + $snapshot[] = '| Data | Commit | Messaggio |'; + $snapshot[] = '| --- | --- | --- |'; + foreach ($recentCommits as $row) { + $snapshot[] = '| ' . $row['date'] . ' | `' . $row['hash'] . '` | ' . str_replace('|', '-', $row['message']) . ' |'; + } + $snapshot[] = ''; + $snapshot[] = '## Note commit recenti'; + $snapshot[] = ''; + if ($commitNotes === []) { + $snapshot[] = '- Nessuna nota commit strutturata disponibile tra gli ultimi commit.'; + } else { + foreach ($commitNotes as $row) { + $snapshot[] = '- `' . $row['hash'] . '` ' . $row['message']; + $snapshot[] = ' - Nota: ' . $row['note']; + } + } + $snapshot[] = ''; + $snapshot[] = '## Pagine toccate di recente'; + $snapshot[] = ''; + if ($recentPages === []) { + $snapshot[] = '- Nessuna pagina recente rilevata.'; + } else { + foreach ($recentPages as $row) { + $snapshot[] = '- `' . $row['commit'] . '` ' . $row['page'] . ' -> `' . $row['path'] . '`'; + } + } + $snapshot[] = ''; + $snapshot[] = '## Fonti da leggere per ripartire'; + $snapshot[] = ''; + $snapshot[] = '- `docs/support/MANUALE-SVILUPPO-OPERATIVO.md`'; + $snapshot[] = '- `docs/support/PBX-PANASONIC-STATO-OPERATIVO.md`'; + $snapshot[] = '- `docs/support/BUG-RISOLTI-E-VALIDAZIONI.md`'; + $snapshot[] = '- `docs/support/CONTINUITA-SVILUPPO-AGENT.md`'; + + File::put($outputPath, implode(PHP_EOL, $snapshot) . PHP_EOL); + File::put($jsonPath, json_encode([ + 'generated_at' => now()->toIso8601String(), + 'branch' => $branch, + 'commit' => $commit, + 'output_path' => $outputPath, + 'source_files' => [ + 'docs/support/MANUALE-SVILUPPO-OPERATIVO.md', + 'docs/support/PBX-PANASONIC-STATO-OPERATIVO.md', + 'docs/support/BUG-RISOLTI-E-VALIDAZIONI.md', + 'docs/support/CONTINUITA-SVILUPPO-AGENT.md', + ], + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + + $this->info('Snapshot continuita sviluppo aggiornata.'); + + return self::SUCCESS; + } + + private function gitValue(array $arguments): ?string + { + $result = Process::path(base_path()) + ->timeout(120) + ->run(array_merge(['git'], $arguments)); + + if (! $result->successful()) { + return null; + } + + $output = trim((string) $result->output()); + + return $output !== '' ? $output : null; + } + + /** @return array */ + private function recentCommits(): array + { + $rows = []; + $result = Process::path(base_path())->timeout(120)->run([ + 'git', 'log', '-n', '8', '--date=format:%d/%m/%Y %H:%M', '--pretty=format:%h|%ad|%s', + ]); + + if (! $result->successful()) { + return []; + } + + $lines = preg_split('/\r\n|\r|\n/', trim((string) $result->output())) ?: []; + foreach ($lines as $line) { + $parts = explode('|', $line, 3); + if (count($parts) !== 3) { + continue; + } + + $rows[] = [ + 'hash' => (string) $parts[0], + 'date' => (string) $parts[1], + 'message' => (string) $parts[2], + ]; + } + + return $rows; + } + + /** @return array */ + private function recentPages(): array + { + $rows = []; + $seen = []; + $result = Process::path(base_path())->timeout(120)->run([ + 'git', 'log', '-n', '12', '--name-only', '--pretty=format:@@@%h', '--', 'app/Filament/Pages', 'resources/views/filament/pages', + ]); + + if (! $result->successful()) { + return []; + } + + $currentCommit = null; + $lines = preg_split('/\r\n|\r|\n/', (string) $result->output()) ?: []; + foreach ($lines as $line) { + $line = trim((string) $line); + if ($line === '') { + continue; + } + + if (str_starts_with($line, '@@@')) { + $currentCommit = substr($line, 3); + continue; + } + + if ($currentCommit === null || isset($seen[$line])) { + continue; + } + + if (! str_starts_with($line, 'app/Filament/Pages/') && ! str_starts_with($line, 'resources/views/filament/pages/')) { + continue; + } + + $seen[$line] = true; + $rows[] = [ + 'page' => basename($line), + 'path' => $line, + 'commit' => $currentCommit, + ]; + + if (count($rows) >= 10) { + break; + } + } + + return $rows; + } + + /** @return array */ + private function recentCommitNotes(): array + { + $path = storage_path('app/support/commit-notes.json'); + if (! is_file($path)) { + return []; + } + + $decoded = json_decode((string) @file_get_contents($path), true); + if (! is_array($decoded)) { + return []; + } + + $rows = []; + foreach ($this->recentCommits() as $commit) { + $fullHash = $this->gitValue(['rev-parse', $commit['hash']]); + if (! is_string($fullHash) || ! isset($decoded[$fullHash]) || ! is_string($decoded[$fullHash])) { + continue; + } + + $rows[] = [ + 'hash' => $commit['hash'], + 'message' => $commit['message'], + 'note' => trim($decoded[$fullHash]), + ]; + } + + return array_slice($rows, 0, 5); + } + + private function extractSection(string $markdown, string $heading): string + { + if ($markdown === '' || ! str_contains($markdown, $heading)) { + return '- Sezione non disponibile.'; + } + + $offset = strpos($markdown, $heading); + if ($offset === false) { + return '- Sezione non disponibile.'; + } + + $slice = substr($markdown, $offset + strlen($heading)); + if (! is_string($slice)) { + return '- Sezione non disponibile.'; + } + + if (preg_match('/\n##\s+/', $slice, $match, PREG_OFFSET_CAPTURE) === 1) { + $slice = substr($slice, 0, (int) $match[0][1]); + } + + return trim($slice) !== '' ? trim($slice) : '- Sezione non disponibile.'; + } + + private function normalizeSectionLines(string $text): string + { + $lines = preg_split('/\r\n|\r|\n/', trim($text)) ?: []; + $cleaned = []; + + foreach ($lines as $line) { + $line = trim((string) $line); + if ($line === '') { + continue; + } + + $cleaned[] = $line; + } + + return $cleaned === [] ? '- Nessuna informazione disponibile.' : implode(PHP_EOL, $cleaned); + } +} \ No newline at end of file diff --git a/app/Console/Commands/NetgesconGitSyncCommand.php b/app/Console/Commands/NetgesconGitSyncCommand.php new file mode 100644 index 0000000..fccb076 --- /dev/null +++ b/app/Console/Commands/NetgesconGitSyncCommand.php @@ -0,0 +1,262 @@ + Modifiche'; + + public function handle(): int + { + $remote = trim((string) $this->option('remote')) ?: 'origin'; + $branch = trim((string) $this->option('branch')) ?: 'main'; + $force = (bool) $this->option('force'); + + if (! is_dir(base_path('.git'))) { + $this->error('Repository Git non trovato nella base path corrente.'); + $this->writeProgress(100, 'Repository Git non trovato', 'failed', 1); + $this->persistSummary([ + 'status' => 'failed', + 'exit_code' => 1, + 'message' => 'Repository Git non trovato', + ]); + + return self::FAILURE; + } + + $this->writeProgress(5, 'Preparazione sync da Gitea', 'running'); + + $currentBranch = $this->runGit(['rev-parse', '--abbrev-ref', 'HEAD']); + $currentCommit = $this->runGit(['rev-parse', '--short', 'HEAD']); + + if ($currentBranch === null || $currentCommit === null) { + $this->error('Impossibile leggere lo stato Git locale.'); + $this->writeProgress(100, 'Impossibile leggere stato Git locale', 'failed', 1); + $this->persistSummary([ + 'status' => 'failed', + 'exit_code' => 1, + 'message' => 'Impossibile leggere stato Git locale', + ]); + + return self::FAILURE; + } + + $dirtyResult = Process::path(base_path())->run(['git', 'status', '--porcelain']); + $isDirty = trim((string) $dirtyResult->output()) !== ''; + + if ($isDirty && ! $force) { + $this->error('Working tree sporco: sync bloccata per evitare conflitti locali.'); + $this->writeProgress(100, 'Working tree sporco: sync bloccata', 'failed', 1); + $this->persistSummary([ + 'status' => 'failed', + 'exit_code' => 1, + 'message' => 'Working tree sporco: sync bloccata', + 'remote' => $remote, + 'branch' => $branch, + 'before_commit' => $currentCommit, + 'working_tree_dirty' => true, + ]); + + return self::FAILURE; + } + + $this->info("Fetch da {$remote}..."); + $this->writeProgress(18, 'Fetch remoto da Gitea', 'running'); + + $fetch = Process::path(base_path()) + ->timeout(1800) + ->run(['git', 'fetch', $remote]); + + if (! $fetch->successful()) { + $this->line(trim((string) ($fetch->errorOutput() !== '' ? $fetch->errorOutput() : $fetch->output()))); + $this->writeProgress(100, 'Fetch remoto fallito', 'failed', 1); + $this->persistSummary([ + 'status' => 'failed', + 'exit_code' => 1, + 'message' => 'Fetch remoto fallito', + 'remote' => $remote, + 'branch' => $branch, + 'before_commit' => $currentCommit, + 'working_tree_dirty' => false, + ]); + + return self::FAILURE; + } + + $this->writeProgress(32, 'Allineamento branch locale', 'running'); + + if ($currentBranch !== $branch) { + $checkout = Process::path(base_path()) + ->timeout(1800) + ->run(['git', 'checkout', $branch]); + + if (! $checkout->successful()) { + $this->line(trim((string) ($checkout->errorOutput() !== '' ? $checkout->errorOutput() : $checkout->output()))); + $this->writeProgress(100, 'Checkout branch fallito', 'failed', 1); + $this->persistSummary([ + 'status' => 'failed', + 'exit_code' => 1, + 'message' => 'Checkout branch fallito', + 'remote' => $remote, + 'branch' => $branch, + 'before_commit' => $currentCommit, + 'working_tree_dirty' => false, + ]); + + return self::FAILURE; + } + } + + $this->info("Pull ff-only da {$remote}/{$branch}..."); + $this->writeProgress(48, 'Pull ff-only da Gitea', 'running'); + + $pull = Process::path(base_path()) + ->timeout(1800) + ->run(['git', 'pull', '--ff-only', $remote, $branch]); + + if (! $pull->successful()) { + $this->line(trim((string) ($pull->errorOutput() !== '' ? $pull->errorOutput() : $pull->output()))); + $this->writeProgress(100, 'Pull da Gitea fallito', 'failed', 1); + $this->persistSummary([ + 'status' => 'failed', + 'exit_code' => 1, + 'message' => 'Pull da Gitea fallito', + 'remote' => $remote, + 'branch' => $branch, + 'before_commit' => $currentCommit, + 'working_tree_dirty' => false, + ]); + + return self::FAILURE; + } + + $this->writeProgress(68, 'Rigenerazione changelog Git automatico', 'running'); + File::ensureDirectoryExists(storage_path('app/support/generated')); + + $docsSync = Process::path(base_path()) + ->timeout(1800) + ->run([ + 'bash', + 'scripts/ops/netgescon-sync-git-changelog.sh', + '--repo', + base_path(), + '--output', + storage_path('app/support/generated/NETGESCON-GIT-AUTOCHANGELOG.md'), + ]); + + if (! $docsSync->successful()) { + $this->line(trim((string) ($docsSync->errorOutput() !== '' ? $docsSync->errorOutput() : $docsSync->output()))); + $this->writeProgress(100, 'Rigenerazione changelog Git fallita', 'failed', 1); + $this->persistSummary([ + 'status' => 'failed', + 'exit_code' => 1, + 'message' => 'Rigenerazione changelog Git fallita', + 'remote' => $remote, + 'branch' => $branch, + 'before_commit' => $currentCommit, + 'working_tree_dirty' => false, + ]); + + return self::FAILURE; + } + + $this->writeProgress(76, 'Rigenerazione snapshot continuita sviluppo', 'running'); + $snapshotExit = Artisan::call('netgescon:development-snapshot'); + if ($snapshotExit !== 0) { + $this->writeProgress(100, 'Rigenerazione snapshot sviluppo fallita', 'failed', 1); + $this->persistSummary([ + 'status' => 'failed', + 'exit_code' => 1, + 'message' => 'Rigenerazione snapshot sviluppo fallita', + 'remote' => $remote, + 'branch' => $branch, + 'before_commit' => $currentCommit, + 'working_tree_dirty' => false, + ]); + + return self::FAILURE; + } + + $this->writeProgress(82, 'Pulizia cache applicativa', 'running'); + $this->callSilently('optimize:clear'); + + $newCommit = $this->runGit(['rev-parse', '--short', 'HEAD']) ?? $currentCommit; + $remoteRef = $this->runGit(['rev-parse', '--short', 'refs/remotes/' . $remote . '/' . $branch]) ?? $newCommit; + + $this->persistSummary([ + 'status' => 'completed', + 'exit_code' => 0, + 'message' => 'Sync Git completata', + 'remote' => $remote, + 'branch' => $branch, + 'before_commit' => $currentCommit, + 'after_commit' => $newCommit, + 'remote_commit' => $remoteRef, + 'working_tree_dirty' => false, + ]); + + $this->info('Sync Git completata con successo.'); + $this->writeProgress(100, 'Sync Git completata', 'completed', 0); + + return self::SUCCESS; + } + + private function runGit(array $arguments): ?string + { + $result = Process::path(base_path()) + ->timeout(1800) + ->run(array_merge(['git'], $arguments)); + + if (! $result->successful()) { + return null; + } + + $output = trim((string) $result->output()); + + return $output !== '' ? $output : null; + } + + private function writeProgress(int $percent, string $message, string $status, ?int $exitCode = null): void + { + $path = trim((string) $this->option('progress-file')); + if ($path === '') { + return; + } + + $dir = dirname($path); + if (! is_dir($dir)) { + @mkdir($dir, 0775, true); + } + + @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)); + } + + private function persistSummary(array $payload): void + { + File::ensureDirectoryExists(storage_path('app/support/generated')); + + @file_put_contents( + storage_path('app/support/generated/git-sync-last.json'), + json_encode(array_merge([ + 'synced_at' => now()->toIso8601String(), + ], $payload), JSON_PRETTY_PRINT | 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 4d63cb9..5313297 100644 --- a/app/Filament/Pages/Supporto/Modifiche.php +++ b/app/Filament/Pages/Supporto/Modifiche.php @@ -42,6 +42,8 @@ class Modifiche extends Page public bool $updateDryRun = false; + public bool $updateSkipBackup = false; + public ?int $lastUpdateExitCode = null; public ?string $lastUpdateOutput = null; @@ -70,7 +72,49 @@ class Modifiche extends Page public string $updateProgressStatus = 'idle'; - public string $activeTab = 'errori'; + public string $gitRemote = 'origin'; + + public string $gitBranch = 'main'; + + public bool $gitForce = false; + + public ?string $gitCurrentBranch = null; + + public ?string $gitCurrentCommit = null; + + public ?string $gitRemoteCommit = null; + + public bool $gitWorkingTreeDirty = false; + + public string $gitAheadBehind = '-'; + + public bool $gitSyncInProgress = false; + + public ?string $gitSyncJobId = null; + + public int $gitSyncProgressPercent = 0; + + public string $gitSyncProgressMessage = 'In attesa'; + + public string $gitSyncProgressStatus = 'idle'; + + public ?int $lastGitSyncExitCode = null; + + public ?string $lastGitSyncOutput = null; + + public ?string $lastGitSyncAt = null; + + public ?string $lastGitSyncIssue = null; + + public ?string $lastGitDocsSyncAt = null; + + public ?string $developmentSnapshotHtml = null; + + public ?string $developmentSnapshotPath = null; + + public ?string $developmentSnapshotGeneratedAt = null; + + public string $activeTab = 'manuale'; public ?string $selectedCommitHash = null; @@ -110,6 +154,9 @@ class Modifiche extends Page /** @var array */ public array $functionalHighlights = []; + /** @var array */ + public array $manualSections = []; + public function mount(): void { $this->reloadDashboardData(); @@ -142,6 +189,9 @@ public function reloadDashboardData(): void $this->loadRuntimeErrors(); $this->loadRecentFilamentPages(); $this->loadFunctionalHighlights(); + $this->loadManualSections(); + $this->loadDevelopmentSnapshot(); + $this->loadGitWorkspaceStatus(); $this->loadLatestBackupSummary(); if ($this->selectedCommitHash === null && isset($this->latestCommits[0]['hash'])) { @@ -183,6 +233,53 @@ public function runUpdateFallback(): void $this->startUpdateJob(true); } + public function runGitSync(): void + { + if (! $this->canRunUpdate()) { + Notification::make() + ->title('Permessi insufficienti') + ->body('Solo super-admin/admin/amministratore possono lanciare la sync da Gitea.') + ->danger() + ->send(); + + return; + } + + $this->startGitSyncJob(); + } + + public function generateDevelopmentSnapshot(): void + { + if (! $this->canRunUpdate()) { + Notification::make() + ->title('Permessi insufficienti') + ->body('Solo super-admin/admin/amministratore possono rigenerare la snapshot di sviluppo.') + ->danger() + ->send(); + + return; + } + + $exitCode = Artisan::call('netgescon:development-snapshot'); + if ($exitCode !== 0) { + Notification::make() + ->title('Errore snapshot sviluppo') + ->body(trim((string) Artisan::output()) ?: 'Rigenerazione non riuscita.') + ->danger() + ->send(); + + return; + } + + $this->reloadDashboardData(); + + Notification::make() + ->title('Snapshot sviluppo aggiornata') + ->body('Il riepilogo di continuita e stato rigenerato per questa macchina.') + ->success() + ->send(); + } + public function refreshUpdateProgress(): void { $jobId = trim((string) ($this->updateJobId ?? '')); @@ -221,6 +318,53 @@ public function refreshUpdateProgress(): void } } + public function refreshGitSyncProgress(): void + { + $jobId = trim((string) ($this->gitSyncJobId ?? '')); + if ($jobId === '') { + return; + } + + $progressPath = storage_path('app/support/git-sync-jobs/' . $jobId . '.progress.json'); + $logPath = storage_path('app/support/git-sync-jobs/' . $jobId . '.log'); + + if (! is_file($progressPath)) { + return; + } + + $decoded = json_decode((string) @file_get_contents($progressPath), true); + if (! is_array($decoded)) { + return; + } + + $this->gitSyncProgressPercent = (int) ($decoded['percent'] ?? $this->gitSyncProgressPercent); + $this->gitSyncProgressMessage = (string) ($decoded['message'] ?? $this->gitSyncProgressMessage); + $this->gitSyncProgressStatus = (string) ($decoded['status'] ?? $this->gitSyncProgressStatus); + + if (! in_array($this->gitSyncProgressStatus, ['completed', 'failed'], true)) { + return; + } + + $exitCode = isset($decoded['exit_code']) && is_numeric($decoded['exit_code']) + ? (int) $decoded['exit_code'] + : ($this->gitSyncProgressStatus === 'completed' ? 0 : 1); + + $this->lastGitSyncExitCode = $exitCode; + $this->lastGitSyncAt = now()->format('d/m/Y H:i:s'); + $this->lastGitSyncOutput = is_file($logPath) ? trim((string) @file_get_contents($logPath)) : ($this->gitSyncProgressMessage ?: ''); + $this->lastGitSyncIssue = $exitCode === 0 ? null : $this->detectGitSyncIssue((string) ($this->lastGitSyncOutput ?? '')); + $this->gitSyncInProgress = false; + + if (is_file(storage_path('app/support/generated/git-sync-last.json'))) { + $summary = json_decode((string) @file_get_contents(storage_path('app/support/generated/git-sync-last.json')), true); + if (is_array($summary) && isset($summary['synced_at'])) { + $this->lastGitDocsSyncAt = (string) $summary['synced_at']; + } + } + + $this->reloadDashboardData(); + } + public function runMaintenanceOptimizeClear(): void { if (! $this->canRunUpdate()) { @@ -289,12 +433,16 @@ public function getUpdatePlannedStepsProperty(): array $steps = [ 'Verifica canale update: ' . $this->updateChannel, - 'Backup pre-update automatico (snapshot differenziale + indice record)', - $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'), + $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 + ? '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')), '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', @@ -439,13 +587,17 @@ private function startUpdateJob(bool $fallback): void $this->updateJobId = $jobId; $this->updateInProgress = true; $this->updateProgressPercent = 1; - $this->updateProgressMessage = 'Preparazione backup pre-update...'; + $this->updateProgressMessage = $this->updateSkipBackup + ? 'Preparazione aggiornamento senza backup (modalita test)...' + : 'Preparazione backup pre-update...'; $this->updateProgressStatus = 'running'; @file_put_contents($progressPath, json_encode([ 'timestamp' => now()->toIso8601String(), 'percent' => 1, - 'message' => 'Preparazione backup pre-update...', + 'message' => $this->updateSkipBackup + ? 'Preparazione aggiornamento senza backup (modalita test)...' + : 'Preparazione backup pre-update...', 'status' => 'running', 'exit_code' => null, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); @@ -454,6 +606,11 @@ private function startUpdateJob(bool $fallback): void $requireDrive = $this->shouldRequireDrivePreupdateBackup(); $driveEnabled = $this->shouldAttemptDrivePreupdateBackup(); + if ($this->updateSkipBackup) { + $requireDrive = false; + $driveEnabled = false; + } + if ($requireDrive && $adminId <= 0) { $this->updateInProgress = false; $this->updateProgressStatus = 'failed'; @@ -472,58 +629,62 @@ private function startUpdateJob(bool $fallback): void return; } - $backupParams = [ - '--differential' => true, - '--tag' => 'update-' . $jobId, - ]; + if (! $this->updateSkipBackup) { + $backupParams = [ + '--differential' => true, + '--tag' => 'update-' . $jobId, + ]; - if ($driveEnabled && $adminId > 0) { - $backupParams['--drive'] = true; - $backupParams['--admin-id'] = (string) $adminId; + if ($driveEnabled && $adminId > 0) { + $backupParams['--drive'] = true; + $backupParams['--admin-id'] = (string) $adminId; - if ($requireDrive) { - $backupParams['--require-drive'] = true; + if ($requireDrive) { + $backupParams['--require-drive'] = true; + } } - } - try { - $backupExit = Artisan::call('netgescon:preupdate-backup', $backupParams); - $backupOut = trim((string) Artisan::output()); - if ($backupExit !== 0) { + try { + $backupExit = Artisan::call('netgescon:preupdate-backup', $backupParams); + $backupOut = trim((string) Artisan::output()); + if ($backupExit !== 0) { + $this->updateInProgress = false; + $this->updateProgressStatus = 'failed'; + $this->updateProgressPercent = 100; + $this->updateProgressMessage = 'Backup pre-update fallito'; + $this->lastUpdateExitCode = 1; + $this->lastUpdateOutput = $backupOut; + $this->lastUpdateIssue = 'Backup pre-update fallito: aggiornamento bloccato per sicurezza.'; + @file_put_contents($progressPath, json_encode([ + 'timestamp' => now()->toIso8601String(), + 'percent' => 100, + 'message' => 'Backup pre-update fallito', + 'status' => 'failed', + 'exit_code' => 1, + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + $this->loadLatestBackupSummary(); + return; + } + $this->loadLatestBackupSummary(); + } catch (Throwable $e) { $this->updateInProgress = false; $this->updateProgressStatus = 'failed'; $this->updateProgressPercent = 100; - $this->updateProgressMessage = 'Backup pre-update fallito'; + $this->updateProgressMessage = 'Eccezione backup pre-update'; $this->lastUpdateExitCode = 1; - $this->lastUpdateOutput = $backupOut; - $this->lastUpdateIssue = 'Backup pre-update fallito: aggiornamento bloccato per sicurezza.'; + $this->lastUpdateOutput = $e->getMessage(); + $this->lastUpdateIssue = 'Eccezione durante backup pre-update: aggiornamento bloccato.'; @file_put_contents($progressPath, json_encode([ 'timestamp' => now()->toIso8601String(), 'percent' => 100, - 'message' => 'Backup pre-update fallito', + 'message' => 'Eccezione backup pre-update', 'status' => 'failed', 'exit_code' => 1, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); - $this->loadLatestBackupSummary(); return; } - $this->loadLatestBackupSummary(); - } catch (Throwable $e) { - $this->updateInProgress = false; - $this->updateProgressStatus = 'failed'; - $this->updateProgressPercent = 100; - $this->updateProgressMessage = 'Eccezione backup pre-update'; - $this->lastUpdateExitCode = 1; - $this->lastUpdateOutput = $e->getMessage(); - $this->lastUpdateIssue = 'Eccezione durante backup pre-update: aggiornamento bloccato.'; - @file_put_contents($progressPath, json_encode([ - 'timestamp' => now()->toIso8601String(), - 'percent' => 100, - 'message' => 'Eccezione backup pre-update', - 'status' => 'failed', - 'exit_code' => 1, - ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); - return; + } else { + $this->appendSupportEvent('update_skip_backup', 'Backup pre-update saltato', 'Modalita test/debug attivata da Supporto > Modifiche'); } $args = [ @@ -563,7 +724,9 @@ private function startUpdateJob(bool $fallback): void Notification::make() ->title('Aggiornamento avviato') - ->body('Backup pre-update completato. Avanzamento visibile in tempo reale.') + ->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.') ->success() ->send(); } @@ -731,6 +894,59 @@ private function loadRecentFilamentPages(): void } } + private function loadGitWorkspaceStatus(): void + { + $this->gitCurrentBranch = $this->runGitProcess(['rev-parse', '--abbrev-ref', 'HEAD']); + $this->gitCurrentCommit = $this->runGitProcess(['rev-parse', '--short', 'HEAD']); + $this->gitRemoteCommit = $this->runGitProcess(['rev-parse', '--short', 'refs/remotes/' . $this->gitRemote . '/' . $this->gitBranch]); + $this->gitWorkingTreeDirty = trim((string) ($this->runGitProcess(['status', '--porcelain']) ?? '')) !== ''; + + $aheadBehind = $this->runGitProcess(['rev-list', '--left-right', '--count', 'HEAD...refs/remotes/' . $this->gitRemote . '/' . $this->gitBranch]); + if (is_string($aheadBehind) && preg_match('/^(\d+)\s+(\d+)$/', trim($aheadBehind), $matches) === 1) { + $this->gitAheadBehind = 'ahead ' . $matches[1] . ' / behind ' . $matches[2]; + } else { + $this->gitAheadBehind = '-'; + } + + $summaryPath = storage_path('app/support/generated/git-sync-last.json'); + if (is_file($summaryPath)) { + $summary = json_decode((string) @file_get_contents($summaryPath), true); + if (is_array($summary)) { + $this->lastGitDocsSyncAt = isset($summary['synced_at']) ? (string) $summary['synced_at'] : $this->lastGitDocsSyncAt; + $this->lastGitSyncAt = isset($summary['synced_at']) ? (string) $summary['synced_at'] : $this->lastGitSyncAt; + $this->lastGitSyncExitCode = isset($summary['exit_code']) && is_numeric($summary['exit_code']) + ? (int) $summary['exit_code'] + : $this->lastGitSyncExitCode; + $this->lastGitSyncIssue = isset($summary['status']) && (string) $summary['status'] === 'failed' + ? (string) ($summary['message'] ?? $this->lastGitSyncIssue) + : null; + } + } + } + + private function loadDevelopmentSnapshot(): void + { + $generatedPath = storage_path('app/support/generated/CONTINUITA-SVILUPPO-SNAPSHOT.md'); + $jsonPath = storage_path('app/support/generated/CONTINUITA-SVILUPPO-SNAPSHOT.json'); + $fallbackPath = base_path('docs/support/CONTINUITA-SVILUPPO-AGENT.md'); + + $path = is_file($generatedPath) ? $generatedPath : $fallbackPath; + $raw = is_file($path) + ? (string) @file_get_contents($path) + : '# Snapshot sviluppo non disponibile' . PHP_EOL . PHP_EOL . '- Rigenera la snapshot dalla pagina Supporto > Modifiche.'; + + $this->developmentSnapshotPath = $path; + $this->developmentSnapshotHtml = $this->renderSupportMarkdown($raw); + $this->developmentSnapshotGeneratedAt = null; + + if (is_file($jsonPath)) { + $decoded = json_decode((string) @file_get_contents($jsonPath), true); + if (is_array($decoded) && isset($decoded['generated_at'])) { + $this->developmentSnapshotGeneratedAt = (string) $decoded['generated_at']; + } + } + } + private function loadFunctionalHighlights(): void { $this->functionalHighlights = []; @@ -773,13 +989,178 @@ private function humanizePagePath(string $path): string public function setActiveTab(string $tab): void { - if (! in_array($tab, ['errori', 'commit', 'migliorie'], true)) { + if (! in_array($tab, ['manuale', 'errori', 'commit', 'migliorie'], true)) { return; } $this->activeTab = $tab; } + private function startGitSyncJob(): void + { + $jobId = now()->format('YmdHis') . '-' . Str::lower(Str::random(6)); + $jobsDir = storage_path('app/support/git-sync-jobs'); + if (! is_dir($jobsDir)) { + @mkdir($jobsDir, 0775, true); + } + + $progressPath = $jobsDir . '/' . $jobId . '.progress.json'; + $logPath = $jobsDir . '/' . $jobId . '.log'; + + $this->gitSyncJobId = $jobId; + $this->gitSyncInProgress = true; + $this->gitSyncProgressPercent = 1; + $this->gitSyncProgressMessage = 'Preparazione sync Git da Gitea...'; + $this->gitSyncProgressStatus = 'running'; + + @file_put_contents($progressPath, json_encode([ + 'timestamp' => now()->toIso8601String(), + 'percent' => 1, + 'message' => 'Preparazione sync Git da Gitea...', + 'status' => 'running', + 'exit_code' => null, + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + + $args = [ + 'php', + 'artisan', + 'netgescon:git-sync', + '--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->gitSyncInProgress = false; + $this->gitSyncProgressStatus = 'failed'; + $this->gitSyncProgressPercent = 100; + $this->gitSyncProgressMessage = 'Impossibile avviare sync Git'; + $this->lastGitSyncExitCode = 1; + $this->lastGitSyncOutput = trim((string) ($started->output() !== '' ? $started->output() : $started->errorOutput())); + $this->lastGitSyncIssue = 'Avvio sync Git fallito.'; + + return; + } + + $this->gitSyncProgressPercent = 5; + $this->gitSyncProgressMessage = 'Job sync Git avviato (PID ' . trim($started->output()) . ')'; + $this->gitSyncProgressStatus = 'running'; + + Notification::make() + ->title('Sync Git avviata') + ->body('Il nodo verra allineato da Gitea senza usare il terminale.') + ->success() + ->send(); + } + + private function loadManualSections(): void + { + $this->manualSections = []; + + $documents = [ + [ + 'key' => 'manuale-operativo', + 'title' => 'Manuale operativo sviluppo', + 'description' => 'Quadro principale: come stiamo sviluppando, cosa e stato implementato e come continuare senza ripartire da zero.', + 'path' => 'docs/support/MANUALE-SVILUPPO-OPERATIVO.md', + ], + [ + 'key' => 'pbx-panasonic', + 'title' => 'PBX Panasonic stato operativo', + 'description' => 'Riepilogo strutturato del lavoro fatto su CTI, CSTA, click-to-call, stato attuale e prossimi test sul centralino Panasonic.', + 'path' => 'docs/support/PBX-PANASONIC-STATO-OPERATIVO.md', + ], + [ + 'key' => 'bug-risolti', + 'title' => 'Bug risolti e validazioni', + 'description' => 'Registro sintetico dei problemi corretti, dello stato di verifica e dei punti ancora da testare.', + 'path' => 'docs/support/BUG-RISOLTI-E-VALIDAZIONI.md', + ], + [ + 'key' => 'continuita-agent', + 'title' => 'Continuita sviluppo e agent', + 'description' => 'Regole pratiche per aggiornare documentazione, note commit e materiali utili al riaggancio delle prossime sessioni.', + 'path' => 'docs/support/CONTINUITA-SVILUPPO-AGENT.md', + ], + ]; + + foreach ($documents as $document) { + $relativePath = (string) $document['path']; + $absolutePath = base_path($relativePath); + $exists = is_file($absolutePath); + $raw = $exists + ? (string) @file_get_contents($absolutePath) + : '# Documento non trovato' . PHP_EOL . PHP_EOL . '- Path atteso: `' . $relativePath . '`'; + + $this->manualSections[] = [ + 'key' => (string) $document['key'], + 'title' => (string) $document['title'], + 'description' => (string) $document['description'], + 'path' => $relativePath, + 'exists' => $exists, + 'html' => $this->renderSupportMarkdown($raw), + ]; + } + } + + public function renderSupportMarkdown(string $markdown): string + { + $replacements = [ + '[E!]' => 'E!', + '[U]' => 'U', + '[P]' => 'P', + '[E]' => 'E', + ]; + + return str_replace( + array_keys($replacements), + array_values($replacements), + Str::markdown($markdown) + ); + } + + private function runGitProcess(array $arguments): ?string + { + $result = Process::path(base_path()) + ->timeout(120) + ->run(array_merge(['git'], $arguments)); + + if (! $result->successful()) { + return null; + } + + $output = trim((string) $result->output()); + + return $output !== '' ? $output : null; + } + + private function detectGitSyncIssue(string $output): string + { + $haystack = Str::lower($output); + + if (str_contains($haystack, 'working tree sporco')) { + return 'Il nodo ha modifiche locali non committate: la sync Git e stata bloccata per sicurezza.'; + } + + if (str_contains($haystack, 'not possible to fast-forward') || str_contains($haystack, 'divergent branches')) { + return 'Il branch locale non e in fast-forward rispetto a Gitea. Serve riallineamento controllato.'; + } + + if (str_contains($haystack, 'could not read from remote repository') || str_contains($haystack, 'permission denied')) { + return 'Il nodo non riesce a leggere il repository remoto Gitea. Verificare chiavi SSH o permessi Git.'; + } + + return 'Sync Git terminata con errore tecnico. Controlla l output completo.'; + } + public function setBugFilterStatus(string $status): void { if (! in_array($status, ['all', 'open', 'resolved'], true)) { @@ -912,6 +1293,12 @@ private function detectUpdateIssue(string $output, int $exitCode): ?string $haystack = Str::lower($output); + if (str_contains($haystack, 'manifest endpoint non disponibile sul server update') + || str_contains($haystack, 'manifest endpoint non trovato') + || str_contains($haystack, '/api/v1/distribution/updates/manifest')) { + return 'Il server distribution non espone ancora il manifest update richiesto. Per staging il flusso corretto e usare "Aggiorna staging da Gitea" nel tab Manuale; il canale distribution resta separato e non e ancora il percorso operativo principale.'; + } + if (str_contains($haystack, 'curl error 28') || str_contains($haystack, 'operation timed out') || str_contains($haystack, 'timeout')) { return 'Timeout di rete durante update (cURL error 28). Verifica connettivita server/repository e riprova.'; } diff --git a/bootstrap/app.php b/bootstrap/app.php index 8e2182e..32564ed 100755 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -20,7 +20,10 @@ use App\Console\Commands\ImportGesconF24LegacyCommand; use App\Console\Commands\IstatSetIndiceCommand; use App\Console\Commands\IstatSyncIndiciCommand; +use App\Console\Commands\NetgesconArchiveRegistrySyncCommand; +use App\Console\Commands\NetgesconDevelopmentSnapshotCommand; use App\Console\Commands\NetgesconDistributionPullCommand; +use App\Console\Commands\NetgesconGitSyncCommand; use App\Console\Commands\NetgesconPreupdateBackupCommand; use App\Console\Commands\NetgesconQaUnitaNominativiCommand; use App\Console\Commands\NetgesconRestoreRecordCommand; @@ -56,7 +59,10 @@ GesconImportAlignCommand::class, ImportGesconF24LegacyCommand::class, NetgesconQaUnitaNominativiCommand::class, + NetgesconArchiveRegistrySyncCommand::class, + NetgesconDevelopmentSnapshotCommand::class, NetgesconDistributionPullCommand::class, + NetgesconGitSyncCommand::class, NetgesconPreupdateBackupCommand::class, NetgesconRestoreRecordCommand::class, PanasonicCstaBridgeCommand::class, diff --git a/resources/views/filament/pages/supporto/modifiche.blade.php b/resources/views/filament/pages/supporto/modifiche.blade.php index 8f44d8a..22aa76e 100644 --- a/resources/views/filament/pages/supporto/modifiche.blade.php +++ b/resources/views/filament/pages/supporto/modifiche.blade.php @@ -29,12 +29,19 @@
+