Supporto Modifiche: aggiunge sync staging da Gitea

This commit is contained in:
michele 2026-03-28 17:29:40 +00:00
parent cc1c8f69aa
commit 8ee59acb31
5 changed files with 1220 additions and 62 deletions

View File

@ -0,0 +1,295 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Process;
class NetgesconDevelopmentSnapshotCommand extends Command
{
protected $signature = 'netgescon:development-snapshot
{--output= : Output markdown custom}
{--json= : Output JSON custom}';
protected $description = 'Rigenera una snapshot locale di continuita sviluppo per Supporto > 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<int,array{hash:string,date:string,message:string}> */
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<int,array{page:string,path:string,commit:string}> */
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<int,array{hash:string,message:string,note:string}> */
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);
}
}

View File

@ -0,0 +1,262 @@
<?php
namespace App\Console\Commands;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Process;
class NetgesconGitSyncCommand extends Command
{
protected $signature = 'netgescon:git-sync
{--remote=origin : Remote Git da usare}
{--branch=main : Branch da allineare}
{--force : Continua anche con working tree sporco}
{--progress-file= : File JSON dove scrivere lo stato avanzamento}';
protected $description = 'Allinea il nodo corrente da Gitea/Git e rigenera il materiale automatico per Supporto > 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)
);
}
}

View File

@ -42,6 +42,8 @@ class Modifiche extends Page
public bool $updateDryRun = false; public bool $updateDryRun = false;
public bool $updateSkipBackup = false;
public ?int $lastUpdateExitCode = null; public ?int $lastUpdateExitCode = null;
public ?string $lastUpdateOutput = null; public ?string $lastUpdateOutput = null;
@ -70,7 +72,49 @@ class Modifiche extends Page
public string $updateProgressStatus = 'idle'; 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; public ?string $selectedCommitHash = null;
@ -110,6 +154,9 @@ class Modifiche extends Page
/** @var array<int,string> */ /** @var array<int,string> */
public array $functionalHighlights = []; public array $functionalHighlights = [];
/** @var array<int,array{key:string,title:string,description:string,path:string,exists:bool,html:string}> */
public array $manualSections = [];
public function mount(): void public function mount(): void
{ {
$this->reloadDashboardData(); $this->reloadDashboardData();
@ -142,6 +189,9 @@ public function reloadDashboardData(): void
$this->loadRuntimeErrors(); $this->loadRuntimeErrors();
$this->loadRecentFilamentPages(); $this->loadRecentFilamentPages();
$this->loadFunctionalHighlights(); $this->loadFunctionalHighlights();
$this->loadManualSections();
$this->loadDevelopmentSnapshot();
$this->loadGitWorkspaceStatus();
$this->loadLatestBackupSummary(); $this->loadLatestBackupSummary();
if ($this->selectedCommitHash === null && isset($this->latestCommits[0]['hash'])) { if ($this->selectedCommitHash === null && isset($this->latestCommits[0]['hash'])) {
@ -183,6 +233,53 @@ public function runUpdateFallback(): void
$this->startUpdateJob(true); $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 public function refreshUpdateProgress(): void
{ {
$jobId = trim((string) ($this->updateJobId ?? '')); $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 public function runMaintenanceOptimizeClear(): void
{ {
if (! $this->canRunUpdate()) { if (! $this->canRunUpdate()) {
@ -289,12 +433,16 @@ public function getUpdatePlannedStepsProperty(): array
$steps = [ $steps = [
'Verifica canale update: ' . $this->updateChannel, 'Verifica canale update: ' . $this->updateChannel,
'Backup pre-update automatico (snapshot differenziale + indice record)', $this->updateSkipBackup
$requireDrive ? 'Backup pre-update saltato manualmente (modalita test/debug)'
? 'Upload backup su Google Drive obbligatorio prima dell update' : 'Backup pre-update automatico (snapshot differenziale + indice record)',
: ($driveEnabled $this->updateSkipBackup
? 'Upload backup su Google Drive tentato se configurato sul sito corrente' ? 'Upload backup su Google Drive non eseguito perche il backup e stato saltato'
: 'Upload backup su Google Drive non obbligatorio per questo nodo'), : ($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)'), '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',
@ -439,13 +587,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 = 'Preparazione backup pre-update...'; $this->updateProgressMessage = $this->updateSkipBackup
? 'Preparazione aggiornamento senza backup (modalita test)...'
: 'Preparazione backup pre-update...';
$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' => 'Preparazione backup pre-update...', 'message' => $this->updateSkipBackup
? 'Preparazione aggiornamento senza backup (modalita test)...'
: 'Preparazione backup pre-update...',
'status' => 'running', 'status' => 'running',
'exit_code' => null, 'exit_code' => null,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
@ -454,6 +606,11 @@ private function startUpdateJob(bool $fallback): void
$requireDrive = $this->shouldRequireDrivePreupdateBackup(); $requireDrive = $this->shouldRequireDrivePreupdateBackup();
$driveEnabled = $this->shouldAttemptDrivePreupdateBackup(); $driveEnabled = $this->shouldAttemptDrivePreupdateBackup();
if ($this->updateSkipBackup) {
$requireDrive = false;
$driveEnabled = false;
}
if ($requireDrive && $adminId <= 0) { if ($requireDrive && $adminId <= 0) {
$this->updateInProgress = false; $this->updateInProgress = false;
$this->updateProgressStatus = 'failed'; $this->updateProgressStatus = 'failed';
@ -472,58 +629,62 @@ private function startUpdateJob(bool $fallback): void
return; return;
} }
$backupParams = [ if (! $this->updateSkipBackup) {
'--differential' => true, $backupParams = [
'--tag' => 'update-' . $jobId, '--differential' => true,
]; '--tag' => 'update-' . $jobId,
];
if ($driveEnabled && $adminId > 0) { if ($driveEnabled && $adminId > 0) {
$backupParams['--drive'] = true; $backupParams['--drive'] = true;
$backupParams['--admin-id'] = (string) $adminId; $backupParams['--admin-id'] = (string) $adminId;
if ($requireDrive) { if ($requireDrive) {
$backupParams['--require-drive'] = true; $backupParams['--require-drive'] = true;
}
} }
}
try { try {
$backupExit = Artisan::call('netgescon:preupdate-backup', $backupParams); $backupExit = Artisan::call('netgescon:preupdate-backup', $backupParams);
$backupOut = trim((string) Artisan::output()); $backupOut = trim((string) Artisan::output());
if ($backupExit !== 0) { 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->updateInProgress = false;
$this->updateProgressStatus = 'failed'; $this->updateProgressStatus = 'failed';
$this->updateProgressPercent = 100; $this->updateProgressPercent = 100;
$this->updateProgressMessage = 'Backup pre-update fallito'; $this->updateProgressMessage = 'Eccezione backup pre-update';
$this->lastUpdateExitCode = 1; $this->lastUpdateExitCode = 1;
$this->lastUpdateOutput = $backupOut; $this->lastUpdateOutput = $e->getMessage();
$this->lastUpdateIssue = 'Backup pre-update fallito: aggiornamento bloccato per sicurezza.'; $this->lastUpdateIssue = 'Eccezione durante backup pre-update: aggiornamento bloccato.';
@file_put_contents($progressPath, json_encode([ @file_put_contents($progressPath, json_encode([
'timestamp' => now()->toIso8601String(), 'timestamp' => now()->toIso8601String(),
'percent' => 100, 'percent' => 100,
'message' => 'Backup pre-update fallito', 'message' => 'Eccezione backup pre-update',
'status' => 'failed', 'status' => 'failed',
'exit_code' => 1, 'exit_code' => 1,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$this->loadLatestBackupSummary();
return; return;
} }
$this->loadLatestBackupSummary(); } else {
} catch (Throwable $e) { $this->appendSupportEvent('update_skip_backup', 'Backup pre-update saltato', 'Modalita test/debug attivata da Supporto > Modifiche');
$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;
} }
$args = [ $args = [
@ -563,7 +724,9 @@ private function startUpdateJob(bool $fallback): void
Notification::make() Notification::make()
->title('Aggiornamento avviato') ->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() ->success()
->send(); ->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 private function loadFunctionalHighlights(): void
{ {
$this->functionalHighlights = []; $this->functionalHighlights = [];
@ -773,13 +989,178 @@ private function humanizePagePath(string $path): string
public function setActiveTab(string $tab): void public function setActiveTab(string $tab): void
{ {
if (! in_array($tab, ['errori', 'commit', 'migliorie'], true)) { if (! in_array($tab, ['manuale', 'errori', 'commit', 'migliorie'], true)) {
return; return;
} }
$this->activeTab = $tab; $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!]' => '<span class="inline-flex items-center gap-2"><span class="inline-block h-3 w-3 rounded-sm bg-danger-500"></span><span class="font-mono">E!</span></span>',
'[U]' => '<span class="inline-flex items-center gap-2"><span class="inline-block h-3 w-3 rounded-sm bg-success-500"></span><span class="font-mono">U</span></span>',
'[P]' => '<span class="inline-flex items-center gap-2"><span class="inline-block h-3 w-3 rounded-sm bg-primary-500"></span><span class="font-mono">P</span></span>',
'[E]' => '<span class="inline-flex items-center gap-2"><span class="inline-block h-3 w-3 rounded-sm bg-warning-500"></span><span class="font-mono">E</span></span>',
];
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 public function setBugFilterStatus(string $status): void
{ {
if (! in_array($status, ['all', 'open', 'resolved'], true)) { if (! in_array($status, ['all', 'open', 'resolved'], true)) {
@ -912,6 +1293,12 @@ private function detectUpdateIssue(string $output, int $exitCode): ?string
$haystack = Str::lower($output); $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')) { 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.'; return 'Timeout di rete durante update (cURL error 28). Verifica connettivita server/repository e riprova.';
} }

View File

@ -20,7 +20,10 @@
use App\Console\Commands\ImportGesconF24LegacyCommand; use App\Console\Commands\ImportGesconF24LegacyCommand;
use App\Console\Commands\IstatSetIndiceCommand; use App\Console\Commands\IstatSetIndiceCommand;
use App\Console\Commands\IstatSyncIndiciCommand; use App\Console\Commands\IstatSyncIndiciCommand;
use App\Console\Commands\NetgesconArchiveRegistrySyncCommand;
use App\Console\Commands\NetgesconDevelopmentSnapshotCommand;
use App\Console\Commands\NetgesconDistributionPullCommand; use App\Console\Commands\NetgesconDistributionPullCommand;
use App\Console\Commands\NetgesconGitSyncCommand;
use App\Console\Commands\NetgesconPreupdateBackupCommand; use App\Console\Commands\NetgesconPreupdateBackupCommand;
use App\Console\Commands\NetgesconQaUnitaNominativiCommand; use App\Console\Commands\NetgesconQaUnitaNominativiCommand;
use App\Console\Commands\NetgesconRestoreRecordCommand; use App\Console\Commands\NetgesconRestoreRecordCommand;
@ -56,7 +59,10 @@
GesconImportAlignCommand::class, GesconImportAlignCommand::class,
ImportGesconF24LegacyCommand::class, ImportGesconF24LegacyCommand::class,
NetgesconQaUnitaNominativiCommand::class, NetgesconQaUnitaNominativiCommand::class,
NetgesconArchiveRegistrySyncCommand::class,
NetgesconDevelopmentSnapshotCommand::class,
NetgesconDistributionPullCommand::class, NetgesconDistributionPullCommand::class,
NetgesconGitSyncCommand::class,
NetgesconPreupdateBackupCommand::class, NetgesconPreupdateBackupCommand::class,
NetgesconRestoreRecordCommand::class, NetgesconRestoreRecordCommand::class,
PanasonicCstaBridgeCommand::class, PanasonicCstaBridgeCommand::class,

View File

@ -29,12 +29,19 @@
</div> </div>
<div class="mt-3 flex flex-wrap gap-2"> <div class="mt-3 flex flex-wrap gap-2">
<button
type="button"
wire:click="setActiveTab('manuale')"
class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $this->activeTab === 'manuale' ? 'bg-slate-800 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200' }}"
>
Manuale
</button>
<button <button
type="button" type="button"
wire:click="setActiveTab('errori')" wire:click="setActiveTab('errori')"
class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $this->activeTab === 'errori' ? 'bg-slate-800 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200' }}" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $this->activeTab === 'errori' ? 'bg-slate-800 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200' }}"
> >
Errori Aggiornamenti + Bug
</button> </button>
<button <button
type="button" type="button"
@ -53,11 +60,215 @@ class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $t
</div> </div>
</x-filament::section> </x-filament::section>
@if($this->activeTab === 'manuale')
<x-filament::section class="!p-4">
<div class="grid gap-3 lg:grid-cols-[1.35fr_0.65fr]">
<div class="rounded-lg border bg-slate-50 p-3">
<div class="text-xs font-semibold text-slate-800">Manuale operativo integrato nel progetto</div>
<div class="mt-2 text-[11px] leading-relaxed text-slate-700">
Questa sezione diventa il punto di continuita tra sviluppo, Git e aggiornamenti applicativi. I contenuti sotto arrivano da file Markdown versionati nel repository, quindi restano leggibili sia dalla UI sia durante le prossime sessioni di sviluppo con agent e strumenti locali.
</div>
<div class="mt-3 flex flex-wrap gap-2">
<x-filament::button size="sm" color="gray" wire:click="reloadDashboardData">
Ricarica manuale e dati pagina
</x-filament::button>
<x-filament::button size="sm" color="info" wire:click="setActiveTab('commit')">
Vai a Commit + Note
</x-filament::button>
<x-filament::button size="sm" color="warning" wire:click="setActiveTab('errori')">
Vai a Aggiornamenti + Bug
</x-filament::button>
<x-filament::button size="sm" wire:click="runUpdate" :disabled="!$this->canRunUpdate()">
Lancia aggiornamento
</x-filament::button>
</div>
</div>
<div class="rounded-lg border bg-white p-3">
<div class="text-xs font-semibold text-slate-800">Flusso minimo dopo ogni Git</div>
<ol class="mt-2 list-decimal space-y-1 pl-4 text-[11px] text-slate-700">
<li>aggiorna il documento della sezione corretta nel repository</li>
<li>salva una nota operativa sul commit se serve contesto extra</li>
<li>spingi il codice su Gitea</li>
<li>usa il pulsante di sync da Gitea da questa pagina per allineare staging</li>
<li>se c e un problema applicativo, registralo o segnalo come risolto nel tab BUG</li>
</ol>
</div>
</div>
</x-filament::section>
<x-filament::section class="!p-4">
<div class="mb-2 flex flex-wrap items-start justify-between gap-2">
<div>
<div class="text-xs font-semibold text-gray-700">Snapshot continuita sviluppo</div>
<div class="mt-1 text-[11px] text-gray-500">
Riepilogo automatico per ripartire senza ricostruire ogni volta ragionamenti, file chiave, stato PBX e punti ancora aperti.
</div>
</div>
<div class="flex flex-wrap gap-2">
<div class="rounded bg-slate-100 px-2 py-1 font-mono text-[10px] text-slate-600">{{ $this->developmentSnapshotPath ?? '-' }}</div>
<x-filament::button size="sm" color="gray" wire:click="generateDevelopmentSnapshot" :disabled="!$this->canRunUpdate()">
Rigenera snapshot sviluppo
</x-filament::button>
</div>
</div>
<div class="mb-3 rounded-lg border bg-gray-50 p-3 text-[11px] text-gray-700">
Generata il: <span class="font-medium">{{ $this->developmentSnapshotGeneratedAt ?? '-' }}</span>
</div>
<div class="prose prose-sm max-w-none text-[11px] leading-relaxed">
{!! $this->developmentSnapshotHtml !!}
</div>
</x-filament::section>
<x-filament::section class="!p-4">
<div class="grid gap-3 lg:grid-cols-[1.15fr_0.85fr]">
<div class="rounded-lg border bg-gray-50 p-3 lg:col-span-2">
<div class="mb-2 text-xs font-semibold text-gray-700">Aggiornamento staging da Gitea</div>
<div class="mb-3 text-[11px] text-gray-600">
Questo e il flusso principale da usare sui nodi interni: pull da Gitea, rigenerazione changelog Git per la pagina supporto e pulizia cache applicativa. Non richiede terminale.
</div>
<div class="grid gap-2 sm:grid-cols-4">
<label class="block">
<span class="mb-1 block text-[11px] font-medium">Remote</span>
<input type="text" wire:model="gitRemote" class="w-full rounded-md border-gray-300 text-xs" />
</label>
<label class="block">
<span class="mb-1 block text-[11px] font-medium">Branch</span>
<input type="text" wire:model="gitBranch" class="w-full rounded-md border-gray-300 text-xs" />
</label>
<label class="inline-flex items-center gap-2 rounded-md border bg-white px-2 py-1.5 text-[11px] sm:mt-6">
<input type="checkbox" wire:model="gitForce" class="rounded border-gray-300" />
Force con working tree sporco
</label>
</div>
<div class="mt-3 flex flex-wrap gap-2">
<x-filament::button size="sm" wire:click="runGitSync" :disabled="!$this->canRunUpdate()">
Aggiorna staging da Gitea
</x-filament::button>
<x-filament::button size="sm" color="info" wire:click="refreshGitSyncProgress" :disabled="!$this->gitSyncInProgress">
Aggiorna avanzamento Git
</x-filament::button>
<x-filament::button size="sm" color="gray" wire:click="reloadDashboardData">
Rileggi stato Git
</x-filament::button>
</div>
@if($this->gitSyncInProgress)
<div class="mt-3 rounded-lg border bg-white p-3" wire:poll.2s="refreshGitSyncProgress">
@else
<div class="mt-3 rounded-lg border bg-white p-3">
@endif
<div class="mb-1 flex items-center justify-between text-[11px] text-gray-600">
<span class="font-semibold">Avanzamento sync Git</span>
<span class="font-mono">{{ (int) $this->gitSyncProgressPercent }}%</span>
</div>
<div class="h-2 w-full overflow-hidden rounded bg-gray-200">
<div class="h-2 {{ $this->gitSyncProgressStatus === 'failed' ? 'bg-rose-500' : 'bg-emerald-500' }}" style="width: {{ max(0, min(100, (int) $this->gitSyncProgressPercent)) }}%"></div>
</div>
<div class="mt-1 text-[11px] text-gray-600">{{ $this->gitSyncProgressMessage }}</div>
<div class="mt-1 text-[10px] text-gray-500">
Job: <span class="font-mono">{{ $this->gitSyncJobId ?? '-' }}</span>
· Stato: <span class="font-semibold">{{ $this->gitSyncProgressStatus }}</span>
@if($this->gitSyncInProgress)
· in esecuzione
@endif
</div>
</div>
</div>
<div class="rounded-lg border bg-white p-3">
<div class="text-xs font-semibold text-gray-700">Stato repository nodo</div>
<div class="mt-2 space-y-1 text-[11px] text-gray-600">
<div>Branch locale: <span class="font-mono font-medium">{{ $this->gitCurrentBranch ?? '-' }}</span></div>
<div>Commit locale: <span class="font-mono font-medium">{{ $this->gitCurrentCommit ?? '-' }}</span></div>
<div>Commit remoto: <span class="font-mono font-medium">{{ $this->gitRemoteCommit ?? '-' }}</span></div>
<div>Stato tracking: <span class="font-medium">{{ $this->gitAheadBehind }}</span></div>
<div>
Working tree:
<span class="font-medium {{ $this->gitWorkingTreeDirty ? 'text-rose-700' : 'text-emerald-700' }}">{{ $this->gitWorkingTreeDirty ? 'sporco' : 'pulito' }}</span>
</div>
<div>Ultima sync Git: <span class="font-medium">{{ $this->lastGitSyncAt ?? '-' }}</span></div>
<div>Rigenerazione changelog locale: <span class="font-medium">{{ $this->lastGitDocsSyncAt ?? '-' }}</span></div>
</div>
</div>
<div class="rounded-lg border bg-white p-3">
<div class="text-xs font-semibold text-gray-700">Esito ultima sync Git</div>
<div class="mt-2 space-y-1 text-[11px] text-gray-600">
<div>
Exit code:
@if($this->lastGitSyncExitCode === null)
<span class="font-medium">-</span>
@elseif($this->lastGitSyncExitCode === 0)
<span class="rounded bg-emerald-100 px-1.5 py-0.5 font-semibold text-emerald-800">0 OK</span>
@else
<span class="rounded bg-rose-100 px-1.5 py-0.5 font-semibold text-rose-800">{{ $this->lastGitSyncExitCode }} ERRORE</span>
@endif
</div>
</div>
@if(filled($this->lastGitSyncIssue))
<div class="mt-3 rounded border border-amber-300 bg-amber-50 px-2 py-2 text-[11px] text-amber-900">
{{ $this->lastGitSyncIssue }}
</div>
@endif
</div>
</div>
@if(filled($this->lastGitSyncOutput))
<div class="mt-3 rounded-lg border bg-slate-950 p-3">
<div class="mb-1 text-[11px] font-semibold text-slate-200">Output tecnico sync Git</div>
<pre class="max-h-64 overflow-auto whitespace-pre-wrap text-[11px] leading-relaxed text-slate-100">{{ $this->lastGitSyncOutput }}</pre>
</div>
@endif
</x-filament::section>
@foreach($this->manualSections as $section)
<x-filament::section class="!p-4">
<div class="mb-2 flex flex-wrap items-start justify-between gap-2">
<div>
<div class="text-xs font-semibold text-gray-700">{{ $section['title'] }}</div>
<div class="mt-1 text-[11px] text-gray-500">{{ $section['description'] }}</div>
</div>
<div class="rounded bg-slate-100 px-2 py-1 font-mono text-[10px] text-slate-600">{{ $section['path'] }}</div>
</div>
@if(! $section['exists'])
<div class="mb-3 rounded border border-amber-300 bg-amber-50 px-3 py-2 text-[11px] text-amber-900">
Documento non ancora presente nel repository. La sezione e predisposta per essere popolata progressivamente.
</div>
@endif
<div class="prose prose-sm max-w-none text-[11px] leading-relaxed">
{!! $section['html'] !!}
</div>
</x-filament::section>
@endforeach
@endif
@if($this->activeTab === 'errori') @if($this->activeTab === 'errori')
<x-filament::section class="!p-4"> <x-filament::section class="!p-4">
<div class="mb-3 rounded-lg border border-amber-300 bg-amber-50 p-3 text-[11px] text-amber-900">
<div class="font-semibold">Uso corretto di questa sezione</div>
<div class="mt-1">
Per il nodo staging, il percorso operativo principale e la sync Git da Gitea disponibile nel tab <span class="font-semibold">Manuale</span>. I pulsanti sotto servono al canale <span class="font-semibold">distribution</span>, che al momento non e ancora il flusso update attivo principale e puo restituire errore manifest 404.
</div>
<div class="mt-2">
<x-filament::button size="sm" color="warning" wire:click="setActiveTab('manuale')">
Vai a Manuale e sync Gitea
</x-filament::button>
</div>
</div>
<div class="grid gap-3 lg:grid-cols-3"> <div class="grid gap-3 lg:grid-cols-3">
<div class="rounded-lg border bg-gray-50 p-3 lg:col-span-2"> <div class="rounded-lg border bg-gray-50 p-3 lg:col-span-2">
<div class="mb-2 text-xs font-semibold text-gray-700">Aggiornamento da server distribution</div> <div class="mb-2 text-xs font-semibold text-gray-700">Aggiornamento da server distribution</div>
<div class="mb-3 text-[11px] text-gray-500">
Usare questi pulsanti solo quando il server <span class="font-mono">updates.netgescon.it</span> espone correttamente il manifest update. Per staging standard usare il flusso Git/Gitea nel tab Manuale.
</div>
<div class="grid gap-2 sm:grid-cols-3"> <div class="grid gap-2 sm:grid-cols-3">
<label class="block"> <label class="block">
<span class="mb-1 block text-[11px] font-medium">Canale</span> <span class="mb-1 block text-[11px] font-medium">Canale</span>
@ -76,6 +287,13 @@ class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $t
</label> </label>
</div> </div>
<div class="mt-2">
<label class="inline-flex items-center gap-2 rounded-md border border-amber-300 bg-amber-50 px-2 py-1.5 text-[11px] text-amber-900">
<input type="checkbox" wire:model="updateSkipBackup" class="rounded border-gray-300" />
Salta backup pre-update solo per test/debug su staging
</label>
</div>
<div class="mt-3 flex flex-wrap gap-2"> <div class="mt-3 flex flex-wrap gap-2">
<x-filament::button size="sm" wire:click="runUpdate" :disabled="!$this->canRunUpdate()"> <x-filament::button size="sm" wire:click="runUpdate" :disabled="!$this->canRunUpdate()">
Lancia aggiornamento Lancia aggiornamento
@ -411,22 +629,12 @@ class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $t
$path = base_path('docs/NETGESCON-MODIFICHE.md'); $path = base_path('docs/NETGESCON-MODIFICHE.md');
$raw = file_exists($path) ? file_get_contents($path) : "# Modifiche\n\nFile non trovato: {$path}\n"; $raw = file_exists($path) ? file_get_contents($path) : "# Modifiche\n\nFile non trovato: {$path}\n";
$gitPath = base_path('docs/NETGESCON-GIT-AUTOCHANGELOG.md'); $gitGeneratedPath = storage_path('app/support/generated/NETGESCON-GIT-AUTOCHANGELOG.md');
$gitPath = file_exists($gitGeneratedPath) ? $gitGeneratedPath : base_path('docs/NETGESCON-GIT-AUTOCHANGELOG.md');
$gitRaw = file_exists($gitPath) ? file_get_contents($gitPath) : "# Git Auto Changelog\n\nFile non trovato: {$gitPath}\nEsegui: `bash scripts/ops/netgescon-sync-git-changelog.sh`\n"; $gitRaw = file_exists($gitPath) ? file_get_contents($gitPath) : "# Git Auto Changelog\n\nFile non trovato: {$gitPath}\nEsegui: `bash scripts/ops/netgescon-sync-git-changelog.sh`\n";
// Mini-render: sostituisce tag tipo [U], [P], [E], [E!] con un quadratino colorato. $rawHtml = $this->renderSupportMarkdown($raw);
$replacements = [ $gitRawHtml = $this->renderSupportMarkdown($gitRaw);
'[E!]' => '<span class="inline-flex items-center gap-2"><span class="inline-block h-3 w-3 rounded-sm bg-danger-500"></span><span class="font-mono">E!</span></span>',
'[U]' => '<span class="inline-flex items-center gap-2"><span class="inline-block h-3 w-3 rounded-sm bg-success-500"></span><span class="font-mono">U</span></span>',
'[P]' => '<span class="inline-flex items-center gap-2"><span class="inline-block h-3 w-3 rounded-sm bg-primary-500"></span><span class="font-mono">P</span></span>',
'[E]' => '<span class="inline-flex items-center gap-2"><span class="inline-block h-3 w-3 rounded-sm bg-warning-500"></span><span class="font-mono">E</span></span>',
];
$rawHtml = \Illuminate\Support\Str::markdown($raw);
$rawHtml = str_replace(array_keys($replacements), array_values($replacements), $rawHtml);
$gitRawHtml = \Illuminate\Support\Str::markdown($gitRaw);
$gitRawHtml = str_replace(array_keys($replacements), array_values($replacements), $gitRawHtml);
@endphp @endphp
@if($this->activeTab === 'migliorie') @if($this->activeTab === 'migliorie')