Fix support update log memory exhaustion
This commit is contained in:
parent
c4f8c827b4
commit
cd2eed25bc
|
|
@ -5,6 +5,8 @@
|
||||||
use App\Models\Stabile;
|
use App\Models\Stabile;
|
||||||
use App\Models\SupportBugRegistry;
|
use App\Models\SupportBugRegistry;
|
||||||
use App\Models\SupportUpdateEntry;
|
use App\Models\SupportUpdateEntry;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\ProgramAclService;
|
||||||
use App\Support\StabileContext;
|
use App\Support\StabileContext;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
|
|
@ -228,20 +230,23 @@ class Modifiche extends Page
|
||||||
/** @var array<int,array{key:string,title:string,description:string,path:string,exists:bool,html:string}> */
|
/** @var array<int,array{key:string,title:string,description:string,path:string,exists:bool,html:string}> */
|
||||||
public array $manualSections = [];
|
public array $manualSections = [];
|
||||||
|
|
||||||
|
/** @var array<int,array<string,mixed>> */
|
||||||
|
public array $invoiceExtractionProfiles = [];
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
|
$this->updateSkipBackup = ! $this->shouldRunPreupdateBackup();
|
||||||
$this->reloadDashboardData();
|
$this->reloadDashboardData();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
if (! $user) {
|
if (! $user instanceof User) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pagina informativa: accessibile a chi accede al pannello.
|
return app(ProgramAclService::class)->canAccessProgram($user, 'supporto.modifiche');
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function canRunUpdate(): bool
|
public function canRunUpdate(): bool
|
||||||
|
|
@ -258,6 +263,7 @@ public function reloadDashboardData(): void
|
||||||
$this->loadVersionInfo();
|
$this->loadVersionInfo();
|
||||||
$this->loadLatestCommits();
|
$this->loadLatestCommits();
|
||||||
$this->loadCommitNotes();
|
$this->loadCommitNotes();
|
||||||
|
$this->loadInvoiceExtractionProfiles();
|
||||||
$this->loadRecentFilamentPages();
|
$this->loadRecentFilamentPages();
|
||||||
$this->loadFunctionalHighlights();
|
$this->loadFunctionalHighlights();
|
||||||
$this->loadManualSections();
|
$this->loadManualSections();
|
||||||
|
|
@ -283,6 +289,28 @@ public function reloadDashboardData(): void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function loadInvoiceExtractionProfiles(): void
|
||||||
|
{
|
||||||
|
$profiles = config('document_extraction_profiles.profiles', []);
|
||||||
|
$this->invoiceExtractionProfiles = collect(is_array($profiles) ? $profiles : [])
|
||||||
|
->filter(fn($profile): bool => is_array($profile) && ! empty($profile['key']))
|
||||||
|
->map(function (array $profile): array {
|
||||||
|
return [
|
||||||
|
'key' => (string) ($profile['key'] ?? ''),
|
||||||
|
'service' => (string) ($profile['service'] ?? ''),
|
||||||
|
'provider' => (string) ($profile['provider'] ?? ''),
|
||||||
|
'status' => (string) ($profile['status'] ?? 'draft'),
|
||||||
|
'source' => (string) ($profile['source'] ?? ''),
|
||||||
|
'fields' => array_values(array_filter(array_map('strval', (array) ($profile['fields'] ?? [])))),
|
||||||
|
'usage' => array_values(array_filter(array_map('strval', (array) ($profile['usage'] ?? [])))),
|
||||||
|
'notes' => trim((string) ($profile['notes'] ?? '')),
|
||||||
|
'commands' => array_values(array_filter(array_map('strval', (array) ($profile['commands'] ?? [])))),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
public function runUpdate(): void
|
public function runUpdate(): void
|
||||||
{
|
{
|
||||||
if (! $this->canRunUpdate()) {
|
if (! $this->canRunUpdate()) {
|
||||||
|
|
@ -329,6 +357,11 @@ public function runGitSync(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
public function runNodeRefresh(): void
|
public function runNodeRefresh(): void
|
||||||
|
{
|
||||||
|
$this->runAutonomousNodeRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function runAutonomousNodeRefresh(): void
|
||||||
{
|
{
|
||||||
if (! $this->canRunUpdate()) {
|
if (! $this->canRunUpdate()) {
|
||||||
Notification::make()
|
Notification::make()
|
||||||
|
|
@ -340,6 +373,22 @@ public function runNodeRefresh(): void
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->updateInProgress) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Aggiornamento gia in corso')
|
||||||
|
->body('Il nodo sta gia eseguendo un refresh asincrono. Attendi la fine o aggiorna l avanzamento.')
|
||||||
|
->warning()
|
||||||
|
->send();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->shouldUseDaemonUpdateExecutor()) {
|
||||||
|
$this->queueDaemonNodeRefreshRequest();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$this->startNodeRefreshJob();
|
$this->startNodeRefreshJob();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -435,6 +484,20 @@ public function refreshUpdateProgress(): void
|
||||||
|
|
||||||
if ($exitCode !== 0) {
|
if ($exitCode !== 0) {
|
||||||
$this->appendSupportEvent('update_async', 'Aggiornamento asincrono fallito', (string) ($this->lastUpdateOutput ?? ''));
|
$this->appendSupportEvent('update_async', 'Aggiornamento asincrono fallito', (string) ($this->lastUpdateOutput ?? ''));
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('Refresh nodo fallito')
|
||||||
|
->body($this->lastUpdateIssue ?: 'Controlla output completo, log supporto e runtime errors.')
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
} else {
|
||||||
|
$this->appendSupportEvent('update_async', 'Aggiornamento asincrono completato', (string) ($this->lastUpdateOutput ?? ''));
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('Refresh nodo completato')
|
||||||
|
->body('Il nodo e stato riallineato senza accesso al sistema remoto. Backup, maintenance, sync e rebuild risultano completati.')
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->reloadDashboardData();
|
$this->reloadDashboardData();
|
||||||
|
|
@ -578,21 +641,25 @@ public function runMaintenanceMigrate(): void
|
||||||
*/
|
*/
|
||||||
public function getUpdatePlannedStepsProperty(): array
|
public function getUpdatePlannedStepsProperty(): array
|
||||||
{
|
{
|
||||||
$requireDrive = $this->shouldRequireDrivePreupdateBackup();
|
$backupEnabled = $this->shouldRunPreupdateBackup();
|
||||||
$driveEnabled = $this->shouldAttemptDrivePreupdateBackup();
|
$backupVisible = $this->shouldShowPreupdateBackupPlan();
|
||||||
|
$requireDrive = $this->shouldRequireDrivePreupdateBackup();
|
||||||
|
$driveEnabled = $this->shouldAttemptDrivePreupdateBackup();
|
||||||
|
|
||||||
$steps = [
|
$steps = [
|
||||||
'Verifica canale update: ' . $this->updateChannel,
|
'Verifica canale update: ' . $this->updateChannel,
|
||||||
$this->updateSkipBackup
|
$backupEnabled
|
||||||
? 'Backup pre-update saltato manualmente (modalita test/debug)'
|
? 'Backup pre-update automatico (snapshot differenziale + indice record)'
|
||||||
: 'Backup pre-update automatico (snapshot differenziale + indice record)',
|
: ($backupVisible
|
||||||
$this->updateSkipBackup
|
? 'Backup pre-update previsto ma temporaneamente disattivato finche la procedura non e definitiva'
|
||||||
? 'Upload backup su Google Drive non eseguito perche il backup e stato saltato'
|
: 'Backup pre-update non previsto per questo nodo'),
|
||||||
: ($requireDrive
|
$backupEnabled
|
||||||
|
? ($requireDrive
|
||||||
? 'Upload backup su Google Drive obbligatorio prima dell update'
|
? 'Upload backup su Google Drive obbligatorio prima dell update'
|
||||||
: ($driveEnabled
|
: ($driveEnabled
|
||||||
? 'Upload backup su Google Drive tentato se configurato sul sito corrente'
|
? 'Upload backup su Google Drive tentato se configurato sul sito corrente'
|
||||||
: 'Upload backup su Google Drive non obbligatorio per questo nodo')),
|
: 'Upload backup su Google Drive non obbligatorio per questo nodo'))
|
||||||
|
: 'Integrazione Google Drive prevista solo come fase successiva; per ora non viene eseguita durante l aggiornamento',
|
||||||
'Esecuzione in modalita ' . ($this->updateDryRun ? 'dry-run (nessuna scrittura)' : 'apply (aggiornamento reale)'),
|
'Esecuzione in modalita ' . ($this->updateDryRun ? 'dry-run (nessuna scrittura)' : 'apply (aggiornamento reale)'),
|
||||||
'Flag force: ' . ($this->updateForce ? 'abilitato' : 'disabilitato'),
|
'Flag force: ' . ($this->updateForce ? 'abilitato' : 'disabilitato'),
|
||||||
'Check endpoint update consigliato prima del lancio',
|
'Check endpoint update consigliato prima del lancio',
|
||||||
|
|
@ -832,6 +899,8 @@ private function startUpdateJob(bool $fallback): void
|
||||||
{
|
{
|
||||||
$this->registerPlannedUpdateSummary();
|
$this->registerPlannedUpdateSummary();
|
||||||
|
|
||||||
|
$backupEnabled = $this->shouldRunPreupdateBackup();
|
||||||
|
|
||||||
$jobId = now()->format('YmdHis') . '-' . Str::lower(Str::random(6));
|
$jobId = now()->format('YmdHis') . '-' . Str::lower(Str::random(6));
|
||||||
$jobsDir = storage_path('app/support/update-jobs');
|
$jobsDir = storage_path('app/support/update-jobs');
|
||||||
if (! is_dir($jobsDir)) {
|
if (! is_dir($jobsDir)) {
|
||||||
|
|
@ -844,17 +913,17 @@ private function startUpdateJob(bool $fallback): void
|
||||||
$this->updateJobId = $jobId;
|
$this->updateJobId = $jobId;
|
||||||
$this->updateInProgress = true;
|
$this->updateInProgress = true;
|
||||||
$this->updateProgressPercent = 1;
|
$this->updateProgressPercent = 1;
|
||||||
$this->updateProgressMessage = $this->updateSkipBackup
|
$this->updateProgressMessage = $backupEnabled
|
||||||
? 'Preparazione aggiornamento senza backup (modalita test)...'
|
? 'Preparazione backup pre-update...'
|
||||||
: 'Preparazione backup pre-update...';
|
: 'Preparazione aggiornamento con backup solo indicato...';
|
||||||
$this->updateProgressStatus = 'running';
|
$this->updateProgressStatus = 'running';
|
||||||
|
|
||||||
@file_put_contents($progressPath, json_encode([
|
@file_put_contents($progressPath, json_encode([
|
||||||
'timestamp' => now()->toIso8601String(),
|
'timestamp' => now()->toIso8601String(),
|
||||||
'percent' => 1,
|
'percent' => 1,
|
||||||
'message' => $this->updateSkipBackup
|
'message' => $backupEnabled
|
||||||
? 'Preparazione aggiornamento senza backup (modalita test)...'
|
? 'Preparazione backup pre-update...'
|
||||||
: 'Preparazione backup pre-update...',
|
: 'Preparazione aggiornamento con backup solo indicato...',
|
||||||
'status' => 'running',
|
'status' => 'running',
|
||||||
'exit_code' => null,
|
'exit_code' => null,
|
||||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||||
|
|
@ -863,7 +932,7 @@ private function startUpdateJob(bool $fallback): void
|
||||||
$requireDrive = $this->shouldRequireDrivePreupdateBackup();
|
$requireDrive = $this->shouldRequireDrivePreupdateBackup();
|
||||||
$driveEnabled = $this->shouldAttemptDrivePreupdateBackup();
|
$driveEnabled = $this->shouldAttemptDrivePreupdateBackup();
|
||||||
|
|
||||||
if ($this->updateSkipBackup) {
|
if (! $backupEnabled) {
|
||||||
$requireDrive = false;
|
$requireDrive = false;
|
||||||
$driveEnabled = false;
|
$driveEnabled = false;
|
||||||
}
|
}
|
||||||
|
|
@ -886,7 +955,7 @@ private function startUpdateJob(bool $fallback): void
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $this->updateSkipBackup) {
|
if ($backupEnabled) {
|
||||||
$backupParams = [
|
$backupParams = [
|
||||||
'--differential' => true,
|
'--differential' => true,
|
||||||
'--tag' => 'update-' . $jobId,
|
'--tag' => 'update-' . $jobId,
|
||||||
|
|
@ -941,7 +1010,7 @@ private function startUpdateJob(bool $fallback): void
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$this->appendSupportEvent('update_skip_backup', 'Backup pre-update saltato', 'Modalita test/debug attivata da Supporto > Modifiche');
|
$this->appendSupportEvent('update_skip_backup', 'Backup pre-update non eseguito', 'Procedura prevista ma sospesa su questo nodo finche il flusso operativo non viene consolidato.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$args = [
|
$args = [
|
||||||
|
|
@ -956,6 +1025,9 @@ private function startUpdateJob(bool $fallback): void
|
||||||
if ($this->updateDryRun) {
|
if ($this->updateDryRun) {
|
||||||
$args[] = '--dry-run';
|
$args[] = '--dry-run';
|
||||||
}
|
}
|
||||||
|
if (! $backupEnabled) {
|
||||||
|
$args[] = '--skip-backup';
|
||||||
|
}
|
||||||
if ($this->updateForce || $fallback) {
|
if ($this->updateForce || $fallback) {
|
||||||
$args[] = '--force';
|
$args[] = '--force';
|
||||||
}
|
}
|
||||||
|
|
@ -981,9 +1053,9 @@ private function startUpdateJob(bool $fallback): void
|
||||||
|
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->title('Aggiornamento avviato')
|
->title('Aggiornamento avviato')
|
||||||
->body($this->updateSkipBackup
|
->body($backupEnabled
|
||||||
? 'Aggiornamento avviato senza backup pre-update (modalita test). Avanzamento visibile in tempo reale.'
|
? 'Backup pre-update completato. Avanzamento visibile in tempo reale.'
|
||||||
: 'Backup pre-update completato. Avanzamento visibile in tempo reale.')
|
: 'Aggiornamento avviato con backup solo indicato e non eseguito. Avanzamento visibile in tempo reale.')
|
||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
}
|
}
|
||||||
|
|
@ -1012,6 +1084,10 @@ private function resolveAmministratoreId(): int
|
||||||
|
|
||||||
private function shouldAttemptDrivePreupdateBackup(): bool
|
private function shouldAttemptDrivePreupdateBackup(): bool
|
||||||
{
|
{
|
||||||
|
if (! $this->shouldRunPreupdateBackup()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (! (bool) config('distribution.preupdate_drive_enabled', false)) {
|
if (! (bool) config('distribution.preupdate_drive_enabled', false)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -1037,9 +1113,97 @@ private function shouldAttemptDrivePreupdateBackup(): bool
|
||||||
|
|
||||||
private function shouldRequireDrivePreupdateBackup(): bool
|
private function shouldRequireDrivePreupdateBackup(): bool
|
||||||
{
|
{
|
||||||
|
if (! $this->shouldRunPreupdateBackup()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return (bool) config('distribution.preupdate_require_drive', false);
|
return (bool) config('distribution.preupdate_require_drive', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function shouldRunPreupdateBackup(): bool
|
||||||
|
{
|
||||||
|
return (bool) config('distribution.preupdate_backup_enabled', false) && ! $this->updateSkipBackup;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shouldUseDaemonUpdateExecutor(): bool
|
||||||
|
{
|
||||||
|
return trim((string) config('distribution.update_executor', 'local')) === 'daemon';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shouldShowPreupdateBackupPlan(): bool
|
||||||
|
{
|
||||||
|
return (bool) config('distribution.preupdate_backup_visible', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function queueDaemonNodeRefreshRequest(): void
|
||||||
|
{
|
||||||
|
$jobId = now()->format('YmdHis') . '-' . Str::lower(Str::random(6));
|
||||||
|
$jobsDir = storage_path('app/support/update-jobs');
|
||||||
|
$requestsDir = storage_path('app/support/update-requests');
|
||||||
|
|
||||||
|
if (! is_dir($jobsDir)) {
|
||||||
|
@mkdir($jobsDir, 0775, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_dir($requestsDir)) {
|
||||||
|
@mkdir($requestsDir, 0775, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$progressPath = $jobsDir . '/' . $jobId . '.progress.json';
|
||||||
|
$requestPath = $requestsDir . '/' . $jobId . '.json';
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'job_id' => $jobId,
|
||||||
|
'requested_at' => now()->toIso8601String(),
|
||||||
|
'requested_by' => optional(Auth::user())->email,
|
||||||
|
'remote' => trim($this->gitRemote),
|
||||||
|
'branch' => trim($this->gitBranch),
|
||||||
|
'force' => (bool) $this->gitForce,
|
||||||
|
'skip_backup' => ! $this->shouldRunPreupdateBackup(),
|
||||||
|
'site_mode' => (string) config('netgescon.maintenance.site_mode', config('netgescon.site_mode', 'app')),
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->updateJobId = $jobId;
|
||||||
|
$this->updateInProgress = true;
|
||||||
|
$this->updateProgressPercent = 1;
|
||||||
|
$this->updateProgressMessage = 'Richiesta aggiornata accodata al daemon host...';
|
||||||
|
$this->updateProgressStatus = 'queued';
|
||||||
|
|
||||||
|
@file_put_contents($progressPath, json_encode([
|
||||||
|
'timestamp' => now()->toIso8601String(),
|
||||||
|
'percent' => 1,
|
||||||
|
'message' => 'Richiesta accodata al daemon host Docker...',
|
||||||
|
'status' => 'queued',
|
||||||
|
'exit_code' => null,
|
||||||
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||||
|
|
||||||
|
$written = @file_put_contents($requestPath, json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
|
||||||
|
if ($written === false) {
|
||||||
|
$this->updateInProgress = false;
|
||||||
|
$this->updateProgressPercent = 100;
|
||||||
|
$this->updateProgressStatus = 'failed';
|
||||||
|
$this->updateProgressMessage = 'Impossibile creare richiesta daemon';
|
||||||
|
$this->lastUpdateExitCode = 1;
|
||||||
|
$this->lastUpdateIssue = 'Creazione richiesta daemon fallita.';
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('Richiesta daemon fallita')
|
||||||
|
->body('Il nodo non e riuscito a scrivere la richiesta verso il runner host.')
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->appendSupportEvent('update_daemon_request', 'Richiesta update accodata al daemon host', json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: $jobId);
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('Richiesta update accodata')
|
||||||
|
->body('Il daemon host Docker puo eseguire ora il refresh completo senza accesso shell manuale.')
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
private function registerPlannedUpdateSummary(): void
|
private function registerPlannedUpdateSummary(): void
|
||||||
{
|
{
|
||||||
$summary = implode(' | ', $this->getUpdatePlannedStepsProperty());
|
$summary = implode(' | ', $this->getUpdatePlannedStepsProperty());
|
||||||
|
|
@ -1793,12 +1957,16 @@ private function startNodeRefreshJob(bool $skipMaintenance = false, bool $skipCo
|
||||||
$this->updateJobId = $jobId;
|
$this->updateJobId = $jobId;
|
||||||
$this->updateInProgress = true;
|
$this->updateInProgress = true;
|
||||||
$this->updateProgressPercent = 1;
|
$this->updateProgressPercent = 1;
|
||||||
|
$backupEnabled = (bool) config('distribution.preupdate_backup_enabled', false) && ! $this->updateSkipBackup;
|
||||||
|
|
||||||
$this->updateProgressMessage = $skipComposer
|
$this->updateProgressMessage = $skipComposer
|
||||||
? 'Preparazione refresh nodo da Gitea senza maintenance e senza composer...'
|
? 'Preparazione refresh nodo da Gitea senza maintenance e senza composer...'
|
||||||
: ($skipMaintenance
|
: ($skipMaintenance
|
||||||
? 'Preparazione refresh nodo da Gitea senza maintenance mode...'
|
? 'Preparazione refresh nodo da Gitea senza maintenance mode...'
|
||||||
: 'Preparazione refresh nodo da Gitea...');
|
: ($backupEnabled
|
||||||
$this->updateProgressStatus = 'running';
|
? 'Preparazione refresh nodo da Gitea...'
|
||||||
|
: 'Preparazione refresh nodo da Gitea con backup solo indicato...'));
|
||||||
|
$this->updateProgressStatus = 'running';
|
||||||
|
|
||||||
@file_put_contents($progressPath, json_encode([
|
@file_put_contents($progressPath, json_encode([
|
||||||
'timestamp' => now()->toIso8601String(),
|
'timestamp' => now()->toIso8601String(),
|
||||||
|
|
@ -1807,7 +1975,9 @@ private function startNodeRefreshJob(bool $skipMaintenance = false, bool $skipCo
|
||||||
? 'Preparazione refresh nodo da Gitea senza maintenance e senza composer...'
|
? 'Preparazione refresh nodo da Gitea senza maintenance e senza composer...'
|
||||||
: ($skipMaintenance
|
: ($skipMaintenance
|
||||||
? 'Preparazione refresh nodo da Gitea senza maintenance mode...'
|
? 'Preparazione refresh nodo da Gitea senza maintenance mode...'
|
||||||
: 'Preparazione refresh nodo da Gitea...'),
|
: ($backupEnabled
|
||||||
|
? 'Preparazione refresh nodo da Gitea...'
|
||||||
|
: 'Preparazione refresh nodo da Gitea con backup solo indicato...')),
|
||||||
'status' => 'running',
|
'status' => 'running',
|
||||||
'exit_code' => null,
|
'exit_code' => null,
|
||||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||||
|
|
@ -1829,7 +1999,7 @@ private function startNodeRefreshJob(bool $skipMaintenance = false, bool $skipCo
|
||||||
$args[] = '--force';
|
$args[] = '--force';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->updateSkipBackup) {
|
if (! $backupEnabled) {
|
||||||
$args[] = '--skip-backup';
|
$args[] = '--skip-backup';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1873,10 +2043,10 @@ private function startNodeRefreshJob(bool $skipMaintenance = false, bool $skipCo
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->title('Refresh nodo avviato')
|
->title('Refresh nodo avviato')
|
||||||
->body($skipComposer
|
->body($skipComposer
|
||||||
? 'Refresh nodo avviato senza maintenance e senza composer. Usalo solo come recovery su macchine dove vendor non e scrivibile dal processo web.'
|
? 'Refresh nodo avviato senza maintenance e senza composer. Usalo solo come recovery su macchine dove vendor non e scrivibile dal processo web.'
|
||||||
: ($skipMaintenance
|
: ($skipMaintenance
|
||||||
? 'Refresh nodo avviato senza maintenance mode. Usalo solo come percorso di riserva su macchine dove il blocco accessi non si attiva.'
|
? 'Refresh nodo avviato senza maintenance mode. Usalo solo come percorso di riserva su macchine dove il blocco accessi non si attiva.'
|
||||||
: 'Sync da Gitea, migrate e rebuild cache verranno eseguiti direttamente sul nodo da browser.'))
|
: 'Flusso autonomo avviato: backup, maintenance con pagina 503 personalizzata, sync da Gitea, rebuild applicativo e riapertura nodo.'))
|
||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
}
|
}
|
||||||
|
|
@ -2143,6 +2313,34 @@ private function detectUpdateIssue(string $output, int $exitCode): ?string
|
||||||
return 'Permessi insufficienti sul filesystem o su git durante update.';
|
return 'Permessi insufficienti sul filesystem o su git durante update.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (str_contains($haystack, 'backup pre-update fallito')) {
|
||||||
|
return 'Backup pre-update non completato: l aggiornamento e stato fermato per sicurezza prima di modificare il nodo.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($haystack, 'maintenance mode non attivata')) {
|
||||||
|
return 'La maintenance mode Laravel non si e attivata. Verifica che il nodo remoto stia eseguendo il codice aggiornato con il render 503 applicativo.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($haystack, 'sync git fallita')) {
|
||||||
|
return 'La sync da Gitea e fallita. Controlla repository remoto, branch, working tree e permessi Git del nodo.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($haystack, 'composer install fallito')) {
|
||||||
|
return 'Composer non e riuscito a riallineare le dipendenze sul nodo remoto. Verifica permessi, spazio disco e connettivita verso Packagist o mirror configurati.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($haystack, 'migration fallita')) {
|
||||||
|
return 'Le migration non sono state applicate correttamente. Il nodo e stato riportato fuori maintenance, ma serve analizzare l output completo e gli errori runtime registrati.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($haystack, 'view:clear fallito') || str_contains($haystack, 'view:cache fallita')) {
|
||||||
|
return 'La ricostruzione delle view Blade e fallita. Controlla cache, permessi di storage e output completo del refresh nodo.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_contains($haystack, 'nodo ancora in maintenance')) {
|
||||||
|
return 'Il refresh ha finito i passaggi applicativi ma il nodo non e stato riaperto automaticamente. Controlla il log e lo stato maintenance registrato.';
|
||||||
|
}
|
||||||
|
|
||||||
return 'Aggiornamento terminato con errore tecnico. Controlla l\'output completo.';
|
return 'Aggiornamento terminato con errore tecnico. Controlla l\'output completo.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2198,7 +2396,12 @@ private function appendFromNdjson(string $file, string $sourceLabel, array &$eve
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$lines = @file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
$remaining = max(0, 250 - count($events));
|
||||||
|
if ($remaining === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = $this->readTailLines($file, $remaining, 1024 * 1024);
|
||||||
if (! is_array($lines) || $lines === []) {
|
if (! is_array($lines) || $lines === []) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2229,13 +2432,17 @@ private function appendFromLaravelLog(string $file, array &$events): void
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$lines = @file($file, FILE_IGNORE_NEW_LINES);
|
$remaining = max(0, 450 - count($events));
|
||||||
|
if ($remaining === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = $this->readTailLines($file, 4000, 4 * 1024 * 1024);
|
||||||
if (! is_array($lines) || $lines === []) {
|
if (! is_array($lines) || $lines === []) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$slice = array_slice($lines, -4000);
|
foreach ($lines as $line) {
|
||||||
foreach ($slice as $line) {
|
|
||||||
if (! is_string($line) || trim($line) === '') {
|
if (! is_string($line) || trim($line) === '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -2263,6 +2470,66 @@ private function appendFromLaravelLog(string $file, array &$events): void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return array<int, string> */
|
||||||
|
private function readTailLines(string $file, int $lineLimit, int $byteLimit = 1048576): array
|
||||||
|
{
|
||||||
|
if ($lineLimit <= 0 || ! is_file($file)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$handle = @fopen($file, 'rb');
|
||||||
|
if ($handle === false) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$size = @filesize($file);
|
||||||
|
if (! is_int($size) || $size <= 0) {
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$chunkSize = 65536;
|
||||||
|
$buffer = '';
|
||||||
|
$position = $size;
|
||||||
|
$bytesRead = 0;
|
||||||
|
|
||||||
|
while ($position > 0 && $bytesRead < $byteLimit) {
|
||||||
|
$readSize = min($chunkSize, $position, $byteLimit - $bytesRead);
|
||||||
|
if ($readSize <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$position -= $readSize;
|
||||||
|
if (fseek($handle, $position) !== 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$chunk = fread($handle, $readSize);
|
||||||
|
if (! is_string($chunk) || $chunk === '') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$buffer = $chunk . $buffer;
|
||||||
|
$bytesRead += $readSize;
|
||||||
|
|
||||||
|
if (substr_count($buffer, "\n") >= $lineLimit + 1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
$lines = preg_split('/\r\n|\n|\r/', $buffer);
|
||||||
|
if (! is_array($lines) || $lines === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = array_values(array_filter($lines, static fn(mixed $line): bool => is_string($line) && trim($line) !== ''));
|
||||||
|
|
||||||
|
return array_slice($lines, -$lineLimit);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int,array{timestamp:string,type:string,message:string,context:string,source:string}> $events
|
* @param array<int,array{timestamp:string,type:string,message:string,context:string,source:string}> $events
|
||||||
* @return array<int,array{timestamp:string,type:string,message:string,context:string,source:string,fingerprint:string,bug_id:int,bug_code:string,bug_status:string,occurrences:int,resolution_note:string,resolved_at:?string}>
|
* @return array<int,array{timestamp:string,type:string,message:string,context:string,source:string,fingerprint:string,bug_id:int,bug_code:string,bug_status:string,occurrences:int,resolution_note:string,resolved_at:?string}>
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,38 @@ ## Regola finale
|
||||||
- comando tecnico sottostante: `netgescon:git-sync`
|
- comando tecnico sottostante: `netgescon:git-sync`
|
||||||
- documentazione di cio che si sta distribuendo: file Markdown versionati nel repository
|
- documentazione di cio che si sta distribuendo: file Markdown versionati nel repository
|
||||||
|
|
||||||
|
## Emergenza: pagina Supporto > Modifiche in out-of-memory
|
||||||
|
|
||||||
|
Se un nodo remoto va in errore aprendo `admin-filament/supporto/aggiornamento-nodo` o `Supporto > Modifiche` con messaggi tipo `Allowed memory size exhausted`, la prima ipotesi da verificare e la dimensione dei log runtime.
|
||||||
|
|
||||||
|
Controllo rapido consigliato:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/netgescon
|
||||||
|
ls -lh storage/logs/laravel.log storage/logs/runtime-errors.ndjson storage/logs/support-events.ndjson
|
||||||
|
```
|
||||||
|
|
||||||
|
Segnale tipico: `laravel.log` molto grande e pagina che prova a leggere il file tutto in memoria.
|
||||||
|
|
||||||
|
Rimedio corretto a regime:
|
||||||
|
|
||||||
|
1. correggere il repository con lettura tail limitata dei log;
|
||||||
|
2. fare commit e push;
|
||||||
|
3. riallineare il nodo via `Supporto > Modifiche` o `php artisan netgescon:git-sync --remote=origin --branch=main`.
|
||||||
|
|
||||||
|
Solo se il nodo e bloccato e serve rientrare subito prima del fix Git:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/netgescon
|
||||||
|
php -l app/Filament/Pages/Supporto/Modifiche.php
|
||||||
|
php artisan optimize:clear
|
||||||
|
mv storage/logs/laravel.log storage/logs/laravel.log.$(date +%Y%m%d-%H%M%S).bak
|
||||||
|
touch storage/logs/laravel.log
|
||||||
|
chown www-data:www-data storage/logs/laravel.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Nota operativa: questa rotazione manuale e solo una misura di contenimento. Il fix vero deve restare nel codice e poi essere riportato subito su Git, altrimenti il problema puo tornare al prossimo log grande o sul prossimo nodo remoto.
|
||||||
|
|
||||||
## Rilascio operativo 2026-04-06
|
## Rilascio operativo 2026-04-06
|
||||||
|
|
||||||
Questo e il blocco che puo essere aggiornato adesso con la procedura Git-first senza correzioni manuali extra su staging.
|
Questo e il blocco che puo essere aggiornato adesso con la procedura Git-first senza correzioni manuali extra su staging.
|
||||||
|
|
@ -176,3 +208,46 @@ ### Sync fornitori legacy 2026-04-08
|
||||||
- non correggere staging a mano se il problema e gia nel repository
|
- non correggere staging a mano se il problema e gia nel repository
|
||||||
- prima si corregge Day0, poi commit, poi push, poi update UI da Gitea
|
- prima si corregge Day0, poi commit, poi push, poi update UI da Gitea
|
||||||
- se un fix urgente viene fatto su staging per emergenza, va riportato subito nel repository prima del prossimo aggiornamento
|
- se un fix urgente viene fatto su staging per emergenza, va riportato subito nel repository prima del prossimo aggiornamento
|
||||||
|
|
||||||
|
## Stato operativo 2026-05-09
|
||||||
|
|
||||||
|
### Nodi client demo/prod
|
||||||
|
|
||||||
|
Per i nodi live che controllano gli aggiornamenti da browser o da `php artisan netgescon:update`, e stato verificato che il DNS pubblico di `updates.netgescon.it` non basta: il client va in timeout verso `195.110.133.76:443`.
|
||||||
|
|
||||||
|
Allineamento corretto, gia applicato ai container demo/prod:
|
||||||
|
|
||||||
|
```env
|
||||||
|
NETGESCON_UPDATE_RESOLVE=updates.netgescon.it:443:192.168.0.53
|
||||||
|
```
|
||||||
|
|
||||||
|
Questa impostazione va considerata parte del runbook operativo finche il DNS interno non viene risolto alla radice.
|
||||||
|
|
||||||
|
### Server update interno `.53`
|
||||||
|
|
||||||
|
Il problema residuo non e piu sul nodo client ma sulla macchina `192.168.0.53` che ospita `updates.netgescon.it`.
|
||||||
|
|
||||||
|
Verifiche eseguite:
|
||||||
|
|
||||||
|
- `GET /api/v1/distribution/health` risponde `200 OK`;
|
||||||
|
- `GET /api/v1/distribution/updates/manifest?channel=free` risponde `404 Not Found`;
|
||||||
|
- nel checkout attuale della `.53` le route pubbliche `updates/manifest` e `updates/package` non risultano deployate;
|
||||||
|
- in `storage/app/distribution` non risultano manifest pubblicati.
|
||||||
|
|
||||||
|
Conclusione pratica:
|
||||||
|
|
||||||
|
- il client ora punta correttamente alla `.53`;
|
||||||
|
- il server update `.53` non e ancora pronto a servire i manifest.
|
||||||
|
|
||||||
|
### Cosa chiedere all agent sulla `.53`
|
||||||
|
|
||||||
|
L altro agent sulla `.53` deve lavorare su questo elenco minimo:
|
||||||
|
|
||||||
|
1. confrontare `routes/api.php` del checkout `.53` con Day0 e inserire le route pubbliche mancanti del blocco distribution;
|
||||||
|
2. confrontare `app/Http/Controllers/Api/DistributionController.php` e portare la versione che include `updatesManifest()` e `updatesPackage()`;
|
||||||
|
3. verificare la presenza di `config/distribution.php` e del motore update coerente con Day0;
|
||||||
|
4. pubblicare `storage/app/distribution/free/manifest.json` e il relativo zip;
|
||||||
|
5. eseguire `php artisan optimize:clear`;
|
||||||
|
6. validare con `curl` locale verso `health` e `updates/manifest`.
|
||||||
|
|
||||||
|
Se il checkout `.53` resta sporco, evitare sync distruttivi. In quel caso fare merge selettivo solo sui file del motore update oppure preparare un checkout pulito dedicato al server update.
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,14 @@ ## Obiettivo
|
||||||
|
|
||||||
## Registro sintetico
|
## Registro sintetico
|
||||||
|
|
||||||
|
### Supporto > Modifiche in memory exhaustion su log grandi
|
||||||
|
|
||||||
|
- Problema: la pagina `admin-filament/supporto/aggiornamento-nodo` poteva andare in `Allowed memory size exhausted` durante `loadRuntimeErrors()` quando `storage/logs/laravel.log` diventava molto grande. Sul nodo staging il caso reale era un `laravel.log` da circa `278 MB`.
|
||||||
|
- Causa tecnica: `appendFromNdjson()` e `appendFromLaravelLog()` usavano `file(...)`, quindi caricavano l intero file in RAM anche se poi la UI mostrava solo gli ultimi eventi utili.
|
||||||
|
- Fix applicato: in `app/Filament/Pages/Supporto/Modifiche.php` i lettori sono stati sostituiti con una lettura tail limitata per linee e byte (`readTailLines()`), mantenendo il comportamento della UI ma senza allocare l intero log.
|
||||||
|
- Stato: [U] riprodotto e corretto sul nodo locale `/var/www/netgescon`; validazione eseguita via reflection su `loadRuntimeErrors()` con `laravel.log` grande, picco memoria rimasto intorno a `89 MB` e `39` righe BUG caricate correttamente.
|
||||||
|
- Prevenzione: non usare aumento del `memory_limit` come rimedio principale; mantenere logrotate attivo e preferire sempre parsing incrementale o tail bounded per i file operativi letti dalla UI.
|
||||||
|
|
||||||
### Ripristino bootstrap pannello admin
|
### Ripristino bootstrap pannello admin
|
||||||
|
|
||||||
- Problema: `AdminFilamentPanelProvider.php` era stato corrotto da una modifica incompleta e impediva il bootstrap corretto.
|
- Problema: `AdminFilamentPanelProvider.php` era stato corrotto da una modifica incompleta e impediva il bootstrap corretto.
|
||||||
|
|
@ -46,6 +54,7 @@ ## Validazioni eseguite nell ultimo ciclo
|
||||||
- `php artisan list` eseguito con esito positivo dopo gli ultimi fix applicativi
|
- `php artisan list` eseguito con esito positivo dopo gli ultimi fix applicativi
|
||||||
- controlli sintattici PHP sulle classi modificate senza errori
|
- controlli sintattici PHP sulle classi modificate senza errori
|
||||||
- diagnostica editor senza errori sui file toccati
|
- diagnostica editor senza errori sui file toccati
|
||||||
|
- `Supporto > Modifiche`: validato il nuovo parsing tail-bounded dei log su nodo con `laravel.log` da `278 MB`, senza esaurimento memoria
|
||||||
|
|
||||||
## Validazioni ancora aperte
|
## Validazioni ancora aperte
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user