netgescon-master/docs/02-architettura-laravel/specifiche/UPDATE_SYSTEM.md
Pikappa2 480e7eafbd 🎯 NETGESCON - Setup iniziale repository completo
📋 Commit iniziale con:
-  Documentazione unificata in docs/
-  Codice Laravel in netgescon-laravel/
-  Script automazione in scripts/
-  Configurazione sync rsync
-  Struttura organizzata e pulita

🔄 Versione: 2025.07.19-1644
🎯 Sistema pronto per Git distribuito
2025-07-19 16:44:47 +02:00

26 KiB

NetGesCon - Sistema Aggiornamenti Automatici

🎯 Panoramica Sistema

Il sistema di aggiornamenti automatici NetGesCon permette:

  • Registrazione utenti con codici 8 caratteri univoci
  • Download automatico aggiornamenti via API
  • Backup pre-aggiornamento automatico
  • Rollback in caso di errori
  • Gestione versioni (stable/development)
  • Sistema licenze basato su livello servizio

🏗️ Architettura Sistema

NetGesCon Master Server (update.netgescon.com)
├── API Registrazione Utenti
├── API Download Aggiornamenti  
├── Database Utenti/Licenze
├── Repository Versioni
└── Sistema Notifiche

NetGesCon Client (Installazione Locale)
├── Update Service
├── Backup Manager
├── Version Manager
└── License Validator

📊 Database Schema

Tabella: registered_users

CREATE TABLE registered_users (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    codice_utente VARCHAR(8) UNIQUE NOT NULL, -- es: "USR12345"
    email VARCHAR(255) UNIQUE NOT NULL,
    nome VARCHAR(100) NOT NULL,
    cognome VARCHAR(100) NOT NULL,
    azienda VARCHAR(200),
    telefono VARCHAR(20),
    
    -- Licenza e Servizi
    livello_servizio ENUM('basic', 'professional', 'enterprise') DEFAULT 'basic',
    data_scadenza DATE,
    max_amministratori INT DEFAULT 5,
    max_stabili INT DEFAULT 50,
    features_abilitate JSON, -- {"multi_db": true, "audit": false, "api": true}
    
    -- Sicurezza
    api_key VARCHAR(64) UNIQUE,
    api_secret VARCHAR(128),
    ultimo_accesso TIMESTAMP NULL,
    ip_autorizzati TEXT, -- JSON array IP
    
    -- Sistema
    stato ENUM('attivo', 'sospeso', 'scaduto') DEFAULT 'attivo',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP NULL
);

Tabella: system_versions

CREATE TABLE system_versions (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    versione VARCHAR(20) NOT NULL, -- "2.1.0"
    tipo_release ENUM('stable', 'development', 'hotfix') DEFAULT 'stable',
    
    -- Files
    download_url VARCHAR(500),
    checksum_sha256 VARCHAR(64),
    dimensione_mb DECIMAL(8,2),
    
    -- Compatibilità
    versione_php_min VARCHAR(10), -- "8.2"
    versione_laravel_min VARCHAR(10), -- "10.0"
    versione_mysql_min VARCHAR(10), -- "8.0"
    
    -- Descrizione
    titolo VARCHAR(200),
    descrizione TEXT,
    changelog TEXT,
    breaking_changes TEXT,
    
    -- Controllo
    richiede_backup BOOLEAN DEFAULT true,
    richiede_downtime BOOLEAN DEFAULT false,
    compatibile_rollback BOOLEAN DEFAULT true,
    
    -- Metadata
    data_rilascio TIMESTAMP,
    stato ENUM('draft', 'testing', 'published', 'deprecated') DEFAULT 'draft',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

Tabella: update_logs

CREATE TABLE update_logs (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    codice_utente VARCHAR(8),
    versione_da VARCHAR(20),
    versione_a VARCHAR(20),
    
    -- Processo
    stato ENUM('started', 'downloading', 'backing_up', 'installing', 'completed', 'failed', 'rolled_back'),
    percentuale_completamento TINYINT DEFAULT 0,
    
    -- Dettagli
    log_output TEXT,
    errore_dettaglio TEXT,
    backup_path VARCHAR(500),
    tempo_inizio TIMESTAMP,
    tempo_fine TIMESTAMP,
    
    -- Sistema
    ip_client VARCHAR(45),
    user_agent TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

🔌 API Endpoints

1. Registrazione Utente

POST /api/v1/register
Content-Type: application/json

{
    "email": "admin@condominio.it",
    "nome": "Mario",
    "cognome": "Rossi", 
    "azienda": "Amministrazioni Rossi SRL",
    "telefono": "+39 123 456 7890",
    "livello_servizio": "professional"
}

Response:
{
    "success": true,
    "data": {
        "codice_utente": "USR12345",
        "api_key": "a1b2c3d4e5f6...",
        "api_secret": "secret_hash...",
        "scadenza": "2026-07-07",
        "features": {
            "multi_db": true,
            "audit": true,
            "api": true
        }
    }
}

2. Verifica Licenza

GET /api/v1/license/verify
Authorization: Bearer {api_key}
X-API-Secret: {api_secret}

Response:
{
    "valid": true,
    "scadenza": "2026-07-07",
    "giorni_rimanenti": 365,
    "livello": "professional",
    "limiti": {
        "amministratori": 20,
        "stabili": 200
    },
    "features": ["multi_db", "audit", "api"]
}

3. Check Aggiornamenti

GET /api/v1/updates/check
Authorization: Bearer {api_key}
X-Client-Version: 2.0.5
X-Release-Channel: stable

Response:
{
    "update_available": true,
    "latest_version": "2.1.0",
    "download_url": "https://update.netgescon.com/releases/2.1.0/netgescon-2.1.0.zip",
    "checksum": "sha256:a1b2c3...",
    "size_mb": 45.2,
    "changelog": "- Fix bug contabilità\n- Nuova UI dashboard...",
    "breaking_changes": false,
    "requires_backup": true
}

4. Download Aggiornamento

GET /api/v1/updates/download/{version}
Authorization: Bearer {api_key}
X-API-Secret: {api_secret}

Response: [Binary ZIP file with update]

💻 Client Update Service

Comando Artisan: update:check

<?php
// app/Console/Commands/UpdateCheckCommand.php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Services\UpdateService;

class UpdateCheckCommand extends Command
{
    protected $signature = 'update:check 
                          {--force : Force check anche se già controllato di recente}
                          {--channel=stable : Channel release (stable|development)}';
    
    protected $description = 'Controlla aggiornamenti disponibili';

    public function handle(UpdateService $updateService)
    {
        $this->info('🔍 Controllo aggiornamenti NetGesCon...');
        
        $channel = $this->option('channel');
        $force = $this->option('force');
        
        $result = $updateService->checkUpdates($channel, $force);
        
        if ($result['update_available']) {
            $this->info("✅ Aggiornamento disponibile:");
            $this->table(['Campo', 'Valore'], [
                ['Versione attuale', $result['current_version']],
                ['Nuova versione', $result['latest_version']],
                ['Tipo release', $result['release_type']],
                ['Dimensione', $result['size_mb'] . ' MB'],
                ['Richiede backup', $result['requires_backup'] ? 'Sì' : 'No']
            ]);
            
            if ($this->confirm('Vuoi procedere con il download?')) {
                $this->call('update:download', ['version' => $result['latest_version']]);
            }
        } else {
            $this->info('✅ Sistema aggiornato alla versione più recente');
        }
    }
}

Comando Artisan: update:install

<?php

class UpdateInstallCommand extends Command
{
    protected $signature = 'update:install 
                          {version : Versione da installare}
                          {--skip-backup : Salta backup pre-installazione}
                          {--rollback-on-error : Auto-rollback in caso di errore}';

    public function handle(UpdateService $updateService, BackupService $backupService)
    {
        $version = $this->argument('version');
        $skipBackup = $this->option('skip-backup');
        $autoRollback = $this->option('rollback-on-error');
        
        $this->info("🚀 Installazione NetGesCon v{$version}");
        
        // 1. Validazione pre-installazione
        $this->info('📋 Validazione sistema...');
        if (!$updateService->validateSystem($version)) {
            $this->error('❌ Sistema non compatibile con questa versione');
            return 1;
        }
        
        // 2. Backup automatico
        if (!$skipBackup) {
            $this->info('💾 Creazione backup pre-aggiornamento...');
            $backupPath = $backupService->createPreUpdateBackup($version);
            $this->info("Backup salvato: {$backupPath}");
        }
        
        // 3. Download se necessario
        if (!$updateService->isVersionDownloaded($version)) {
            $this->info('⬇️ Download aggiornamento...');
            $updateService->downloadVersion($version, function($progress) {
                $this->output->write("\r📦 Download: {$progress}%");
            });
            $this->newLine();
        }
        
        // 4. Installazione
        $this->info('⚙️ Installazione in corso...');
        
        try {
            $updateService->installVersion($version, function($step, $progress) {
                $this->output->write("\r🔧 {$step}: {$progress}%");
            });
            
            $this->newLine();
            $this->info('✅ Aggiornamento completato con successo!');
            $this->info("NetGesCon aggiornato alla versione {$version}");
            
        } catch (\Exception $e) {
            $this->error("❌ Errore durante l'installazione: " . $e->getMessage());
            
            if ($autoRollback && !$skipBackup) {
                $this->warn('🔄 Avvio rollback automatico...');
                if ($backupService->restore($backupPath)) {
                    $this->info('✅ Rollback completato');
                } else {
                    $this->error('❌ Errore durante il rollback');
                }
            }
            
            return 1;
        }
        
        return 0;
    }
}

🔧 UpdateService Implementation

<?php
// app/Services/UpdateService.php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;

class UpdateService
{
    private $apiBaseUrl;
    private $apiKey;
    private $apiSecret;
    private $currentVersion;
    
    public function __construct()
    {
        $this->apiBaseUrl = config('netgescon.update.api_url');
        $this->apiKey = config('netgescon.update.api_key');
        $this->apiSecret = config('netgescon.update.api_secret');
        $this->currentVersion = config('netgescon.version');
    }
    
    public function checkUpdates(string $channel = 'stable', bool $force = false): array
    {
        // Cache del controllo (evita troppe chiamate API)
        $cacheKey = "updates_check_{$channel}";
        
        if (!$force && cache()->has($cacheKey)) {
            return cache($cacheKey);
        }
        
        try {
            $response = Http::withHeaders([
                'Authorization' => "Bearer {$this->apiKey}",
                'X-Client-Version' => $this->currentVersion,
                'X-Release-Channel' => $channel
            ])->get("{$this->apiBaseUrl}/api/v1/updates/check");
            
            if ($response->successful()) {
                $data = $response->json();
                
                // Cache per 1 ora
                cache([$cacheKey => $data], 3600);
                
                return $data;
            }
            
            throw new \Exception('API non raggiungibile: ' . $response->status());
            
        } catch (\Exception $e) {
            Log::error("Errore controllo aggiornamenti: " . $e->getMessage());
            
            return [
                'update_available' => false,
                'current_version' => $this->currentVersion,
                'error' => $e->getMessage()
            ];
        }
    }
    
    public function downloadVersion(string $version, $progressCallback = null): string
    {
        $downloadPath = storage_path("app/updates/{$version}");
        $zipPath = "{$downloadPath}/netgescon-{$version}.zip";
        
        if (!File::exists($downloadPath)) {
            File::makeDirectory($downloadPath, 0755, true);
        }
        
        $response = Http::withHeaders([
            'Authorization' => "Bearer {$this->apiKey}",
            'X-API-Secret' => $this->apiSecret
        ])->withOptions([
            'sink' => $zipPath,
            'progress' => function($downloadTotal, $downloadedBytes) use ($progressCallback) {
                if ($downloadTotal > 0 && $progressCallback) {
                    $progress = round(($downloadedBytes / $downloadTotal) * 100);
                    $progressCallback($progress);
                }
            }
        ])->get("{$this->apiBaseUrl}/api/v1/updates/download/{$version}");
        
        if (!$response->successful()) {
            throw new \Exception("Errore download versione {$version}");
        }
        
        // Verifica checksum
        $this->verifyChecksum($zipPath, $version);
        
        return $zipPath;
    }
    
    public function installVersion(string $version, $progressCallback = null): void
    {
        $zipPath = storage_path("app/updates/{$version}/netgescon-{$version}.zip");
        $extractPath = storage_path("app/updates/{$version}/extracted");
        
        if (!File::exists($zipPath)) {
            throw new \Exception("File aggiornamento non trovato: {$zipPath}");
        }
        
        // 1. Estrazione
        $progressCallback && $progressCallback('Estrazione files', 10);
        $this->extractUpdate($zipPath, $extractPath);
        
        // 2. Backup configurazioni attuali
        $progressCallback && $progressCallback('Backup configurazioni', 20);
        $this->backupConfigurations();
        
        // 3. Manutenzione mode
        $progressCallback && $progressCallback('Attivazione modalità manutenzione', 30);
        \Artisan::call('down');
        
        try {
            // 4. Aggiornamento files
            $progressCallback && $progressCallback('Aggiornamento files applicazione', 40);
            $this->updateApplicationFiles($extractPath);
            
            // 5. Composer install
            $progressCallback && $progressCallback('Installazione dipendenze', 60);
            $this->runComposerInstall();
            
            // 6. Database migrations
            $progressCallback && $progressCallback('Aggiornamento database', 75);
            \Artisan::call('migrate', ['--force' => true]);
            
            // 7. Cache refresh
            $progressCallback && $progressCallback('Aggiornamento cache', 85);
            $this->refreshCache();
            
            // 8. NPM build
            $progressCallback && $progressCallback('Build assets', 95);
            $this->buildAssets();
            
        } finally {
            // 9. Disattivazione manutenzione
            $progressCallback && $progressCallback('Riattivazione applicazione', 100);
            \Artisan::call('up');
        }
        
        // 10. Aggiornamento versione
        $this->updateVersionFile($version);
        
        // 11. Cleanup
        $this->cleanupUpdateFiles($version);
    }
    
    private function extractUpdate(string $zipPath, string $extractPath): void
    {
        $zip = new \ZipArchive();
        
        if ($zip->open($zipPath) === TRUE) {
            $zip->extractTo($extractPath);
            $zip->close();
        } else {
            throw new \Exception("Impossibile estrarre {$zipPath}");
        }
    }
    
    private function updateApplicationFiles(string $extractPath): void
    {
        // Lista files da NON sovrascrivere
        $preserveFiles = [
            '.env',
            'storage/app/*',
            'storage/logs/*',
            'storage/framework/cache/*',
            'storage/framework/sessions/*',
            'storage/framework/views/*'
        ];
        
        // Copia files (eccetto quelli da preservare)
        File::copyDirectory($extractPath, base_path(), function($path) use ($preserveFiles) {
            foreach ($preserveFiles as $preserve) {
                if (fnmatch($preserve, $path)) {
                    return false; // Non copiare
                }
            }
            return true; // Copia
        });
    }
    
    private function verifyChecksum(string $filePath, string $version): void
    {
        // Ottieni checksum atteso dall'API
        $response = Http::withHeaders([
            'Authorization' => "Bearer {$this->apiKey}"
        ])->get("{$this->apiBaseUrl}/api/v1/updates/checksum/{$version}");
        
        $expectedChecksum = $response->json()['checksum'];
        $actualChecksum = hash_file('sha256', $filePath);
        
        if ($expectedChecksum !== $actualChecksum) {
            throw new \Exception("Checksum non valido per versione {$version}");
        }
    }
}

⚙️ Configurazione Client

Config: config/netgescon.php

<?php

return [
    'version' => env('NETGESCON_VERSION', '2.0.0'),
    
    'update' => [
        'api_url' => env('NETGESCON_UPDATE_API_URL', 'https://update.netgescon.com'),
        'api_key' => env('NETGESCON_UPDATE_API_KEY'),
        'api_secret' => env('NETGESCON_UPDATE_API_SECRET'),
        'check_interval' => env('NETGESCON_UPDATE_CHECK_INTERVAL', 24), // ore
        'auto_backup' => env('NETGESCON_UPDATE_AUTO_BACKUP', true),
        'release_channel' => env('NETGESCON_RELEASE_CHANNEL', 'stable'), // stable|development
    ],
    
    'license' => [
        'codice_utente' => env('NETGESCON_LICENSE_CODE'),
        'livello_servizio' => env('NETGESCON_LICENSE_LEVEL', 'basic'),
        'scadenza' => env('NETGESCON_LICENSE_EXPIRES'),
    ],
];

Environment Variables (.env)

# NetGesCon Update System
NETGESCON_VERSION=2.0.0
NETGESCON_UPDATE_API_URL=https://update.netgescon.com
NETGESCON_UPDATE_API_KEY=your_api_key_here
NETGESCON_UPDATE_API_SECRET=your_api_secret_here
NETGESCON_UPDATE_CHECK_INTERVAL=24
NETGESCON_UPDATE_AUTO_BACKUP=true
NETGESCON_RELEASE_CHANNEL=stable

# License
NETGESCON_LICENSE_CODE=USR12345
NETGESCON_LICENSE_LEVEL=professional
NETGESCON_LICENSE_EXPIRES=2026-07-07

🕒 Scheduling Automatico

app/Console/Kernel.php

protected function schedule(Schedule $schedule)
{
    // Check aggiornamenti automatico (ogni 6 ore)
    $schedule->command('update:check --channel=stable')
             ->everySixHours()
             ->runInBackground()
             ->sendOutputTo(storage_path('logs/update-check.log'));
    
    // Verifica licenza (ogni giorno)
    $schedule->command('license:verify')
             ->daily()
             ->at('02:00');
    
    // Cleanup update files (ogni settimana) 
    $schedule->command('update:cleanup')
             ->weekly()
             ->sundays()
             ->at('03:00');
}

🔔 Sistema Notifiche

Notifica Aggiornamento Disponibile

<?php
// app/Notifications/UpdateAvailableNotification.php

namespace App\Notifications;

use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;

class UpdateAvailableNotification extends Notification
{
    private $updateInfo;
    
    public function __construct(array $updateInfo)
    {
        $this->updateInfo = $updateInfo;
    }
    
    public function via($notifiable)
    {
        return ['mail', 'database'];
    }
    
    public function toMail($notifiable)
    {
        return (new MailMessage)
            ->subject('NetGesCon: Aggiornamento Disponibile')
            ->greeting('Ciao ' . $notifiable->name)
            ->line("È disponibile una nuova versione di NetGesCon: {$this->updateInfo['latest_version']}")
            ->line("Versione attuale: {$this->updateInfo['current_version']}")
            ->line("Novità principali:")
            ->line($this->updateInfo['changelog'])
            ->action('Aggiorna Ora', url('/admin/updates'))
            ->line('Ti consigliamo di aggiornare per avere le ultime funzionalità e correzioni.');
    }
}

📱 Frontend Update Manager

Update Status Component (Vue.js)

<template>
  <div class="update-manager">
    <div v-if="updateAvailable" class="update-banner">
      <div class="update-info">
        <h4>🎉 Aggiornamento Disponibile!</h4>
        <p>NetGesCon v{{ latestVersion }} è pronto per l'installazione</p>
        <small>Versione attuale: v{{ currentVersion }}</small>
      </div>
      
      <div class="update-actions">
        <button @click="viewChangelog" class="btn btn-outline">
          📋 Novità
        </button>
        <button @click="startUpdate" class="btn btn-primary" :disabled="isUpdating">
          ⬆️ Aggiorna Ora
        </button>
      </div>
    </div>
    
    <!-- Progress Modal -->
    <div v-if="isUpdating" class="update-modal">
      <div class="modal-content">
        <h3>⚙️ Aggiornamento in corso...</h3>
        <div class="progress-bar">
          <div class="progress-fill" :style="{width: progress + '%'}"></div>
        </div>
        <p>{{ currentStep }} ({{ progress }}%)</p>
        <div class="update-log">
          <pre v-for="line in updateLog" :key="line">{{ line }}</pre>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      updateAvailable: false,
      currentVersion: '2.0.0',
      latestVersion: null,
      isUpdating: false,
      progress: 0,
      currentStep: '',
      updateLog: []
    }
  },
  
  mounted() {
    this.checkForUpdates();
    // Check ogni ora
    setInterval(this.checkForUpdates, 3600000);
  },
  
  methods: {
    async checkForUpdates() {
      try {
        const response = await axios.get('/api/updates/check');
        this.updateAvailable = response.data.update_available;
        this.latestVersion = response.data.latest_version;
      } catch (error) {
        console.error('Errore controllo aggiornamenti:', error);
      }
    },
    
    async startUpdate() {
      if (!confirm('Sicuro di voler procedere con l\'aggiornamento?')) {
        return;
      }
      
      this.isUpdating = true;
      this.progress = 0;
      this.updateLog = [];
      
      try {
        // Avvia aggiornamento via WebSocket o polling
        await this.performUpdate();
      } catch (error) {
        alert('Errore durante l\'aggiornamento: ' + error.message);
      } finally {
        this.isUpdating = false;
      }
    },
    
    async performUpdate() {
      // Implementa WebSocket o polling per progress real-time
      const response = await axios.post('/api/updates/install', {
        version: this.latestVersion
      });
      
      // Simula progress updates
      const steps = [
        'Download aggiornamento...',
        'Backup sistema...',
        'Installazione files...',
        'Aggiornamento database...',
        'Completamento...'
      ];
      
      for (let i = 0; i < steps.length; i++) {
        this.currentStep = steps[i];
        this.progress = ((i + 1) / steps.length) * 100;
        this.updateLog.push(`[${new Date().toLocaleTimeString()}] ${steps[i]}`);
        await new Promise(resolve => setTimeout(resolve, 2000));
      }
      
      // Ricarica pagina per nuova versione
      setTimeout(() => {
        window.location.reload();
      }, 2000);
    }
  }
}
</script>

🔒 Sicurezza e Validazione

Validazione Checksum

private function validateDownload(string $filePath, string $expectedChecksum): bool
{
    $actualChecksum = hash_file('sha256', $filePath);
    return hash_equals($expectedChecksum, $actualChecksum);
}

Signature Verification (GPG)

private function verifySignature(string $filePath, string $signaturePath): bool
{
    $publicKey = file_get_contents(resource_path('keys/netgescon-public.key'));
    
    // Implementa verifica GPG signature
    // ...
    
    return $isValid;
}

Rate Limiting API

// routes/api.php
Route::middleware(['throttle:10,1'])->group(function () {
    Route::get('/updates/check', [UpdateController::class, 'check']);
    Route::post('/updates/download', [UpdateController::class, 'download']);
});

📊 Monitoring e Analytics

Log Update Events

class UpdateEventLogger
{
    public static function logUpdateStart(string $version): void
    {
        Log::info('Update started', [
            'version_from' => config('netgescon.version'),
            'version_to' => $version,
            'timestamp' => now(),
            'user_ip' => request()->ip()
        ]);
    }
    
    public static function logUpdateComplete(string $version, int $duration): void
    {
        Log::info('Update completed', [
            'version' => $version,
            'duration_seconds' => $duration,
            'timestamp' => now()
        ]);
    }
}

Metriche Sistema

  • Tempo medio aggiornamento
  • Tasso successo/fallimento
  • Versioni più utilizzate
  • Problemi comuni durante update

🎯 Roadmap Implementazione

Fase 1: Foundation (Week 1-2)

  • Database schema design
  • API endpoints base
  • UpdateService implementation
  • Basic Artisan commands

Fase 2: Core Features (Week 3-4)

  • Download e installazione automatica
  • Sistema backup/rollback
  • Frontend update manager
  • Notifiche sistema

Fase 3: Advanced (Week 5-6)

  • Gestione licenze
  • Release channels
  • Security features
  • Monitoring e analytics

Fase 4: Testing & Deployment (Week 7-8)

  • Testing completo
  • Documentation
  • Production deployment
  • User onboarding

Ultima modifica: 7 Luglio 2025