Add browser-driven node refresh workflow
This commit is contained in:
parent
94dce96027
commit
1276cc7128
141
app/Console/Commands/NetgesconNodeRefreshCommand.php
Normal file
141
app/Console/Commands/NetgesconNodeRefreshCommand.php
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class NetgesconNodeRefreshCommand extends Command
|
||||
{
|
||||
protected $signature = 'netgescon:node-refresh
|
||||
{--remote=origin : Remote Git da usare}
|
||||
{--branch=main : Branch da allineare}
|
||||
{--force : Continua anche con working tree sporco lato sync}
|
||||
{--skip-migrate : Salta php artisan migrate --force}
|
||||
{--skip-view-cache : Salta rebuild view cache}
|
||||
{--progress-file= : File JSON dove scrivere lo stato avanzamento}';
|
||||
|
||||
protected $description = 'Aggiorna il nodo da Gitea e applica le manutenzioni post-sync senza usare il terminale';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$remote = trim((string) $this->option('remote')) ?: 'origin';
|
||||
$branch = trim((string) $this->option('branch')) ?: 'main';
|
||||
$force = (bool) $this->option('force');
|
||||
$skipMigrate = (bool) $this->option('skip-migrate');
|
||||
$skipViewCache = (bool) $this->option('skip-view-cache');
|
||||
|
||||
$this->writeProgress(2, 'Avvio refresh nodo da Gitea', 'running');
|
||||
|
||||
$gitParams = [
|
||||
'--remote' => $remote,
|
||||
'--branch' => $branch,
|
||||
];
|
||||
|
||||
if ($force) {
|
||||
$gitParams['--force'] = true;
|
||||
}
|
||||
|
||||
$this->info("Sync Git da {$remote}/{$branch}...");
|
||||
$this->writeProgress(15, 'Sync Git da Gitea in corso', 'running');
|
||||
$gitExit = Artisan::call('netgescon:git-sync', $gitParams);
|
||||
$gitOutput = trim((string) Artisan::output());
|
||||
|
||||
if ($gitExit !== 0) {
|
||||
$this->error('Sync Git fallita.');
|
||||
$this->line($gitOutput);
|
||||
$this->writeProgress(100, 'Sync Git fallita', 'failed', $gitExit);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$fullOutput = [
|
||||
'NODE REFRESH: netgescon:git-sync',
|
||||
$gitOutput,
|
||||
];
|
||||
|
||||
if (! $skipMigrate) {
|
||||
$this->info('Esecuzione migrate --force...');
|
||||
$this->writeProgress(55, 'Applicazione migration', 'running');
|
||||
$migrateExit = Artisan::call('migrate', ['--force' => true]);
|
||||
$migrateOutput = trim((string) Artisan::output());
|
||||
$fullOutput[] = 'NODE REFRESH: php artisan migrate --force';
|
||||
$fullOutput[] = $migrateOutput;
|
||||
|
||||
if ($migrateExit !== 0) {
|
||||
$this->error('Migration fallita.');
|
||||
$this->line($migrateOutput);
|
||||
$this->writeProgress(100, 'Migration fallita', 'failed', $migrateExit);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('Pulizia cache applicative...');
|
||||
$this->writeProgress(72, 'Pulizia cache applicative', 'running');
|
||||
$optimizeExit = Artisan::call('optimize:clear');
|
||||
$optimizeOutput = trim((string) Artisan::output());
|
||||
$fullOutput[] = 'NODE REFRESH: php artisan optimize:clear';
|
||||
$fullOutput[] = $optimizeOutput;
|
||||
|
||||
if ($optimizeExit !== 0) {
|
||||
$this->error('optimize:clear fallito.');
|
||||
$this->line($optimizeOutput);
|
||||
$this->writeProgress(100, 'Pulizia cache fallita', 'failed', $optimizeExit);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if (! $skipViewCache) {
|
||||
$this->info('Ricostruzione view cache...');
|
||||
$this->writeProgress(88, 'Ricostruzione view cache', 'running');
|
||||
|
||||
$viewClearExit = Artisan::call('view:clear');
|
||||
$viewClearOutput = trim((string) Artisan::output());
|
||||
$fullOutput[] = 'NODE REFRESH: php artisan view:clear';
|
||||
$fullOutput[] = $viewClearOutput;
|
||||
|
||||
if ($viewClearExit !== 0) {
|
||||
$this->error('view:clear fallito.');
|
||||
$this->line($viewClearOutput);
|
||||
$this->writeProgress(100, 'view:clear fallito', 'failed', $viewClearExit);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$viewCacheExit = Artisan::call('view:cache');
|
||||
$viewCacheOutput = trim((string) Artisan::output());
|
||||
$fullOutput[] = 'NODE REFRESH: php artisan view:cache';
|
||||
$fullOutput[] = $viewCacheOutput;
|
||||
|
||||
if ($viewCacheExit !== 0) {
|
||||
$this->error('view:cache fallita.');
|
||||
$this->line($viewCacheOutput);
|
||||
$this->writeProgress(100, 'view:cache fallita', 'failed', $viewCacheExit);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
$this->line(implode(PHP_EOL . PHP_EOL, array_filter($fullOutput, static fn(string $chunk): bool => trim($chunk) !== '')));
|
||||
$this->writeProgress(100, 'Refresh nodo completato', 'completed', 0);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function writeProgress(int $percent, string $message, string $status, ?int $exitCode = null): void
|
||||
{
|
||||
$path = trim((string) $this->option('progress-file'));
|
||||
if ($path === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
@file_put_contents($path, json_encode([
|
||||
'timestamp' => now()->toIso8601String(),
|
||||
'percent' => max(0, min(100, $percent)),
|
||||
'message' => $message,
|
||||
'status' => $status,
|
||||
'exit_code' => $exitCode,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
}
|
||||
|
|
@ -223,6 +223,8 @@ class Modifiche extends Page
|
|||
/** @var array<string,mixed>|null */
|
||||
public ?array $lastNotificationSanitization = null;
|
||||
|
||||
public bool $ticketAttachmentMetadataReady = false;
|
||||
|
||||
/** @var array<int,array{key:string,title:string,description:string,path:string,exists:bool,html:string}> */
|
||||
public array $manualSections = [];
|
||||
|
||||
|
|
@ -266,6 +268,7 @@ public function reloadDashboardData(): void
|
|||
$this->loadRuntimeErrors();
|
||||
$this->loadLatestBackupSummary();
|
||||
$this->loadLastNotificationSanitization();
|
||||
$this->loadAttachmentMetadataStatus();
|
||||
|
||||
if ($this->supportUpdateFormRecordedAt === '') {
|
||||
$this->resetSupportUpdateForm();
|
||||
|
|
@ -325,6 +328,21 @@ public function runGitSync(): void
|
|||
$this->startGitSyncJob();
|
||||
}
|
||||
|
||||
public function runNodeRefresh(): void
|
||||
{
|
||||
if (! $this->canRunUpdate()) {
|
||||
Notification::make()
|
||||
->title('Permessi insufficienti')
|
||||
->body('Solo super-admin/admin/amministratore possono lanciare il refresh completo del nodo.')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->startNodeRefreshJob();
|
||||
}
|
||||
|
||||
public function generateDevelopmentSnapshot(): void
|
||||
{
|
||||
if (! $this->canRunUpdate()) {
|
||||
|
|
@ -500,6 +518,31 @@ public function runMaintenanceViewRebuild(): void
|
|||
->send();
|
||||
}
|
||||
|
||||
public function runMaintenanceMigrate(): void
|
||||
{
|
||||
if (! $this->canRunUpdate()) {
|
||||
Notification::make()->title('Permessi insufficienti')->danger()->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$result = Process::path(base_path())->timeout(600)->run(['php', 'artisan', 'migrate', '--force']);
|
||||
$out = trim((string) ($result->output() !== '' ? $result->output() : $result->errorOutput()));
|
||||
|
||||
$this->lastUpdateAt = now()->format('d/m/Y H:i:s');
|
||||
$this->lastUpdateExitCode = $result->exitCode();
|
||||
$this->lastUpdateOutput = 'MANUTENZIONE: migrate --force' . PHP_EOL . $out;
|
||||
$this->lastUpdateIssue = $result->successful() ? null : 'Errore durante migrate --force.';
|
||||
|
||||
if (! $result->successful()) {
|
||||
$this->appendSupportEvent('maintenance', 'migrate fallita', $out);
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title($result->successful() ? 'Migration applicate' : 'Errore migrate')
|
||||
->{$result->successful() ? 'success' : 'danger'}()
|
||||
->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,string>
|
||||
*/
|
||||
|
|
@ -1224,6 +1267,16 @@ private function loadLastNotificationSanitization(): void
|
|||
$this->lastNotificationSanitization = $decoded;
|
||||
}
|
||||
|
||||
private function loadAttachmentMetadataStatus(): void
|
||||
{
|
||||
try {
|
||||
$this->ticketAttachmentMetadataReady = Schema::hasTable('ticket_attachments')
|
||||
&& Schema::hasColumn('ticket_attachments', 'metadata');
|
||||
} catch (Throwable) {
|
||||
$this->ticketAttachmentMetadataReady = false;
|
||||
}
|
||||
}
|
||||
|
||||
private function loadDevelopmentSnapshot(): void
|
||||
{
|
||||
$generatedPath = storage_path('app/support/generated/CONTINUITA-SVILUPPO-SNAPSHOT.md');
|
||||
|
|
@ -1696,6 +1749,71 @@ private function startGitSyncJob(): void
|
|||
->send();
|
||||
}
|
||||
|
||||
private function startNodeRefreshJob(): void
|
||||
{
|
||||
$jobId = now()->format('YmdHis') . '-' . Str::lower(Str::random(6));
|
||||
$jobsDir = storage_path('app/support/update-jobs');
|
||||
if (! is_dir($jobsDir)) {
|
||||
@mkdir($jobsDir, 0775, true);
|
||||
}
|
||||
|
||||
$progressPath = $jobsDir . '/' . $jobId . '.progress.json';
|
||||
$logPath = $jobsDir . '/' . $jobId . '.log';
|
||||
|
||||
$this->updateJobId = $jobId;
|
||||
$this->updateInProgress = true;
|
||||
$this->updateProgressPercent = 1;
|
||||
$this->updateProgressMessage = 'Preparazione refresh nodo da Gitea...';
|
||||
$this->updateProgressStatus = 'running';
|
||||
|
||||
@file_put_contents($progressPath, json_encode([
|
||||
'timestamp' => now()->toIso8601String(),
|
||||
'percent' => 1,
|
||||
'message' => 'Preparazione refresh nodo da Gitea...',
|
||||
'status' => 'running',
|
||||
'exit_code' => null,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
$args = [
|
||||
'php',
|
||||
'artisan',
|
||||
'netgescon:node-refresh',
|
||||
'--remote=' . trim($this->gitRemote),
|
||||
'--branch=' . trim($this->gitBranch),
|
||||
'--progress-file=' . $progressPath,
|
||||
];
|
||||
|
||||
if ($this->gitForce) {
|
||||
$args[] = '--force';
|
||||
}
|
||||
|
||||
$escaped = implode(' ', array_map(static fn(string $arg): string => escapeshellarg($arg), $args));
|
||||
$shell = 'nohup ' . $escaped . ' > ' . escapeshellarg($logPath) . ' 2>&1 & echo $!';
|
||||
$started = Process::path(base_path())->run(['bash', '-lc', $shell]);
|
||||
|
||||
if (! $started->successful()) {
|
||||
$this->updateInProgress = false;
|
||||
$this->updateProgressStatus = 'failed';
|
||||
$this->updateProgressPercent = 100;
|
||||
$this->updateProgressMessage = 'Impossibile avviare refresh nodo';
|
||||
$this->lastUpdateExitCode = 1;
|
||||
$this->lastUpdateOutput = trim((string) ($started->output() !== '' ? $started->output() : $started->errorOutput()));
|
||||
$this->lastUpdateIssue = 'Avvio refresh nodo fallito.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->updateProgressPercent = 5;
|
||||
$this->updateProgressMessage = 'Job refresh nodo avviato (PID ' . trim($started->output()) . ')';
|
||||
$this->updateProgressStatus = 'running';
|
||||
|
||||
Notification::make()
|
||||
->title('Refresh nodo avviato')
|
||||
->body('Sync da Gitea, migrate e rebuild cache verranno eseguiti direttamente sul nodo da browser.')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
private function loadManualSections(): void
|
||||
{
|
||||
$this->manualSections = [];
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@
|
|||
<div class="text-sm font-semibold text-slate-900">Sync Git da Gitea</div>
|
||||
<div class="mt-1 text-[11px] text-slate-500">Flusso consigliato per staging e nodi interni.</div>
|
||||
|
||||
@if(! $this->ticketAttachmentMetadataReady)
|
||||
<div class="mt-3 rounded-lg border border-amber-300 bg-amber-50 p-3 text-[11px] text-amber-900">
|
||||
Il nodo non ha ancora la colonna <span class="font-mono">ticket_attachments.metadata</span>. Finche non applichi le migration da questa pagina, GPS ed EXIF dei ticket non possono essere persistiti correttamente.
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-3 grid gap-2 sm:grid-cols-3">
|
||||
<label class="block">
|
||||
<span class="mb-1 block text-[11px] font-medium">Remote</span>
|
||||
|
|
@ -50,6 +56,9 @@
|
|||
</div>
|
||||
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<x-filament::button size="sm" color="success" wire:click="runNodeRefresh" :disabled="!$supportCanRunUpdate">
|
||||
Aggiorna nodo completo da Gitea
|
||||
</x-filament::button>
|
||||
<x-filament::button size="sm" wire:click="runGitSync" :disabled="!$supportCanRunUpdate">
|
||||
Aggiorna staging da Gitea
|
||||
</x-filament::button>
|
||||
|
|
@ -99,7 +108,7 @@
|
|||
</div>
|
||||
|
||||
<div class="mt-3 rounded-lg border border-slate-200 bg-slate-50 p-3 text-[11px] text-slate-600">
|
||||
Git sync e update codice aggiornano i file versionati nel repository. Gli archivi caricati manualmente dentro storage o cartelle private non vengono trasferiti da Git, salvo procedure dedicate di backup/sync dati.
|
||||
Il flusso consigliato del nodo e: push su Gitea, poi "Aggiorna nodo completo da Gitea" da questa pagina. Gli archivi caricati manualmente dentro storage o cartelle private non vengono trasferiti da Git, salvo procedure dedicate di backup/sync dati.
|
||||
</div>
|
||||
|
||||
@if(filled($this->pendingGitPreviewIssue))
|
||||
|
|
@ -138,6 +147,9 @@
|
|||
</div>
|
||||
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<x-filament::button size="sm" color="success" wire:click="runMaintenanceMigrate" :disabled="!$supportCanRunUpdate">
|
||||
Applica migration
|
||||
</x-filament::button>
|
||||
<x-filament::button size="sm" wire:click="runUpdate" :disabled="!$supportCanRunUpdate">
|
||||
Lancia aggiornamento
|
||||
</x-filament::button>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user