diff --git a/.gitea/workflows/README.md b/.gitea/workflows/README.md new file mode 100644 index 0000000..7262958 --- /dev/null +++ b/.gitea/workflows/README.md @@ -0,0 +1,287 @@ +# Gitea Actions per Data-Coupler + +## 📋 Panoramica + +Questo repository utilizza **Gitea Actions** per automatizzare la build e la pubblicazione delle immagini Docker del progetto Data-Coupler. + +## 🔧 Configurazione + +### Prerequisiti + +Per utilizzare Gitea Actions su questo repository, è necessario: + +1. **Gitea Account**: Avere un account su Gitea (gitea.com o istanza self-hosted) +2. **Repository Settings**: Abilitare Gitea Actions nelle impostazioni del repository +3. **Container Registry**: Avere accesso al Gitea Container Registry +4. **Secret Configuration**: Configurare il secret `GITEA_TOKEN` + +### Configurazione del Secret REGISTRY_TOKEN + +Il workflow richiede un token di accesso per pubblicare le immagini Docker sul registry: + +1. Vai su **Settings → Secrets** nel repository Gitea +2. Crea un nuovo secret chiamato `REGISTRY_TOKEN` +3. Il valore deve essere un Personal Access Token con i seguenti permessi: + - `write:packages` - Per pubblicare container images + - `read:packages` - Per leggere images esistenti + +#### Come Creare un Personal Access Token su Gitea + +1. Vai su **Settings → Applications** nel tuo profilo Gitea +2. Clicca su **Generate New Token** +3. Nome del token: `Data-Coupler Docker Build` +4. Seleziona i seguenti scopes: + - `write:packages` + - `read:packages` +5. Clicca su **Generate Token** +6. Copia il token generato (sarà mostrato solo una volta) + +### Configurazione del Repository Path + +Nel file `.gitea/workflows/docker-build.yml`, verifica le variabili di registry: + +```yaml +env: + REGISTRY: gitea.home-nas-ds.org # La tua istanza Gitea self-hosted + IMAGE_NAME: alessio/data-coupler # username/repo sulla tua istanza +``` + +**Importante**: +- `REGISTRY` deve puntare alla tua istanza Gitea con Container Registry abilitato +- `IMAGE_NAME` deve essere nel formato `username/repo` della tua istanza +- Assicurati che il Container Registry sia abilitato su Gitea (Settings → Packages) + +## 🚀 Workflow + +### Docker Build Workflow + +**File**: `.gitea/workflows/docker-build.yml` + +#### Trigger Events + +Il workflow si attiva automaticamente su: +- **Push** sui branch: `main`, `development`, `staging` +- **Manual dispatch** tramite interfaccia web + +#### Jobs + +Il workflow è composto da 3 job principali: + +##### 1. `build-linux` - Build Immagine Linux +- **Runner**: `ubuntu-latest` +- **Dockerfile**: `./Dockerfile` +- **Platform**: `linux/amd64` +- **Tags generati**: + - `latest` (per branch `main` e `development`) + - `development-latest` (per branch `development`) + - `staging-latest` (per branch `staging`) + - `-` (per ogni commit) + - `-` (con data/ora) + +##### 2. `build-windows` - Build Immagine Windows +- **Runner**: `windows-2022` +- **Dockerfile**: `./Dockerfile.windows` +- **Platform**: Windows Server 2022 +- **Tags generati**: Come Linux ma con suffisso `-windows` + +##### 3. `create-manifest` - Multi-Platform Manifest +- **Runner**: `ubuntu-latest` +- **Dipendenze**: `build-linux`, `build-windows` +- Crea manifest multi-piattaforma che combinano le immagini Linux e Windows + +### Strategia di Tagging + +#### Branch `main` +- `latest` - Tag condiviso per versione stabile +- `latest-windows` - Versione Windows +- `main-` - Tag specifico per commit +- `main-` - Tag con timestamp + +#### Branch `development` +- `latest` - Tag condiviso per ultime funzionalità +- `development-latest` - Tag specifico per development +- `latest-windows` / `development-latest-windows` - Versioni Windows +- `development-` - Tag specifico per commit +- `development-` - Tag con timestamp + +#### Branch `staging` +- `staging-latest` - Tag per ambiente di staging +- `staging-latest-windows` - Versione Windows +- `staging-` - Tag specifico per commit +- `staging-` - Tag con timestamp + +## 📦 Utilizzo delle Immagini + +### Pull delle Immagini + +#### Da Gitea Container Registry + +```bash +# Ultima versione stabile (main/development) +docker pull gitea.home-nas-ds.org/alessio/data-coupler:latest + +# Versione development specifica +docker pull gitea.home-nas-ds.org/alessio/data-coupler:development-latest + +# Versione staging +docker pull gitea.home-nas-ds.org/alessio/data-coupler:staging-latest + +# Versione Windows +docker pull gitea.home-nas-ds.org/alessio/data-coupler:latest-windows +``` + +### Docker Compose + +Modifica il `docker-compose.yml` per usare le immagini Gitea: + +```yaml +services: + data-coupler: + image: gitea.home-nas-ds.org/alessio/data-coupler:latest + # ... resto della configurazione +``` + +## 🔍 Monitoraggio + +### Visualizzare lo Stato dei Workflow + +1. Vai nella tab **Actions** del repository Gitea +2. Seleziona il workflow **Build and Push Docker Images** +3. Visualizza i dettagli di ogni esecuzione + +### Log e Debug + +- I log di ogni job sono disponibili nell'interfaccia Gitea Actions +- Per debug dettagliato, attiva il manual dispatch con opzione `force_build` + +## 🔄 Differenze con GitHub Actions + +### Principali Differenze + +1. **Context Variables**: + - Gitea Actions usa le **stesse variabili** di GitHub Actions: `github.*` + - Esempio: `github.ref`, `github.sha`, `github.actor` + - **Nota**: Anche se il servizio è Gitea, le variabili mantengono il prefisso `github` per compatibilità + +2. **Username**: + - Nel workflow è hardcoded come `alessio` per semplicità + - Puoi usare `${{ github.actor }}` se preferisci (utente che ha triggerato il workflow) + +3. **Registry**: + - GitHub: `ghcr.io` → Gitea: `gitea.home-nas-ds.org` (istanza self-hosted) + - Gitea non ha un registry pubblico centralizzato come GitHub + +4. **Secret Name**: + - GitHub: `GITHUB_TOKEN` (automatico) → Gitea: `REGISTRY_TOKEN` (configurato manualmente) + - **Nota**: Il secret non può iniziare con `GITEA_` per limitazioni di Gitea + +4. **Attestation**: + - Il job di attestation non è presente su Gitea (feature GitHub specifica) + +### Compatibilità + +Gitea Actions è compatibile con la maggior parte delle GitHub Actions disponibili su GitHub Marketplace, incluse: +- `actions/checkout@v4` +- `docker/setup-buildx-action@v3` +- `docker/login-action@v3` +- `docker/build-push-action@v5` +- `docker/metadata-action@v5` + +## 🛠️ Troubleshooting + +### Verificare che il Container Registry sia Abilitato + +Prima di tutto, verifica che il Container Registry sia abilitato sulla tua istanza Gitea: + +1. **Controlla la configurazione Gitea** (`app.ini`): + ```ini + [packages] + ENABLED = true + ``` + +2. **Verifica accesso al registry**: + ```bash + curl https://gitea.home-nas-ds.org/v2/ + # Dovrebbe rispondere con status 401 (richiede autenticazione) + # Se ottieni 404, il registry non è abilitato + ``` + +3. **Test autenticazione**: + ```bash + echo "YOUR_TOKEN" | docker login gitea.home-nas-ds.org -u alessio --password-stdin + ``` + +### Errore di Autenticazione + +Se ottieni errori di autenticazione: +1. Verifica che il secret `REGISTRY_TOKEN` sia configurato correttamente +2. Assicurati che il token abbia i permessi `write:packages` +3. Controlla che il token non sia scaduto + +### Build Fallita + +Se la build fallisce: +1. Controlla i log del job specifico +2. Verifica che i Dockerfile siano presenti e corretti +3. Assicurati che le dipendenze NuGet siano accessibili + +### Immagini Non Pubblicate + +Se le immagini non vengono pubblicate: +1. Verifica che `IMAGE_NAME` sia corretto +2. Controlla che il registry sia accessibile +3. Verifica i permessi del token + +## 📚 Risorse + +- [Gitea Actions Documentation](https://docs.gitea.io/en-us/actions/) +- [GitHub Actions Compatibility](https://docs.gitea.io/en-us/usage/actions/comparison/) +- [Docker Build Push Action](https://github.com/docker/build-push-action) + +## 📝 Note + +- **Registry Self-Hosted**: Questo workflow è configurato per usare un'istanza Gitea self-hosted (`gitea.home-nas-ds.org`) +- **Container Registry**: Assicurati che il Container Registry sia abilitato nella tua istanza Gitea (Settings → Packages) +- **Accesso Pubblico/Privato**: Le immagini sono private per default; configura le impostazioni del package per renderle pubbliche se necessario +- **Esecuzione Manuale**: Il workflow supporta anche l'esecuzione manuale tramite `workflow_dispatch` +- **Manifest Multi-Platform**: I manifest multi-platform permettono di usare lo stesso tag per Linux e Windows +- **Alternative Registry**: Se non hai un'istanza Gitea con registry, puoi configurare Docker Hub o altri registry modificando la variabile `REGISTRY` + +### 🐳 Configurazione Registry Alternativi + +Se il Container Registry di Gitea non è disponibile, puoi usare alternative: + +#### Docker Hub +```yaml +env: + REGISTRY: docker.io + IMAGE_NAME: username/data-coupler + +# Nel login step: +- name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} +``` + +#### GitHub Container Registry (come fallback) +```yaml +env: + REGISTRY: ghcr.io + IMAGE_NAME: alessio/data-coupler + +# Nel login step: +- name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} +``` + +--- + +**Versione**: 1.0 +**Ultimo Aggiornamento**: 24 Gennaio 2026 +**Maintainer**: Alessio Dalsanto diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml new file mode 100644 index 0000000..350d436 --- /dev/null +++ b/.gitea/workflows/docker-build.yml @@ -0,0 +1,249 @@ +name: Build and Push Docker Images + +on: + push: + branches: + - main + - development + - staging + workflow_dispatch: + inputs: + force_build: + description: 'Force build even without code changes' + required: false + default: false + type: boolean + +env: + # Gitea Container Registry (self-hosted instance) + REGISTRY: gitea.home-nas-ds.org + # Repository path (format: owner/repo) + IMAGE_NAME: alessio/data-coupler + +jobs: + build-linux: + name: Build Linux Container + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Debug - Check registry access + run: | + echo "Testing registry access..." + curl -v https://gitea.home-nas-ds.org/v2/ || echo "Registry not accessible" + echo "Registry: ${{ env.REGISTRY }}" + echo "Image: ${{ env.IMAGE_NAME }}" + continue-on-error: true + + - name: Debug - Verify secret is configured + run: | + if [ -z "${{ secrets.REGISTRY_TOKEN }}" ]; then + echo "::error::REGISTRY_TOKEN secret is not configured or is empty!" + exit 1 + else + echo "REGISTRY_TOKEN secret is configured (length: ${#REGISTRY_TOKEN})" + fi + env: + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + + - name: Log in to Gitea Container Registry + run: | + echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ${{ env.REGISTRY }} -u alessio --password-stdin + shell: bash + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + # Tag based on branch + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/development' }} + type=raw,value=development-latest,enable=${{ github.ref == 'refs/heads/development' }} + type=raw,value=dev-latest,enable=${{ github.ref == 'refs/heads/dev' }} + type=raw,value=staging-latest,enable=${{ github.ref == 'refs/heads/staging' }} + # Tag with commit sha + type=sha,prefix={{branch}}-,format=short + # Tag with date + type=raw,value={{branch}}-{{date 'YYYYMMDD-HHmmss'}} + flavor: | + latest=false + + - name: Build and push Linux Docker image + id: build + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64 + + build-windows: + name: Build Windows Container + runs-on: windows + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository with Git + run: | + echo ===== Cloning repository from Gitea ===== + git clone --depth 1 --branch ${{ github.ref_name }} https://alessio:%REGISTRY_TOKEN%@gitea.home-nas-ds.org/${{ github.repository }}.git repo + if errorlevel 1 ( + echo Failed to clone repository + exit /b 1 + ) + + echo ===== Moving files to working directory ===== + robocopy repo . /E /MOVE /NFL /NDL /NJH /NJS + REM robocopy exit codes: 0=no copy, 1=ok, 2=extra files, 3=ok+extra, 4+=error + if %ERRORLEVEL% GEQ 8 ( + echo Robocopy failed with exit code %ERRORLEVEL% + exit /b 1 + ) + rmdir repo /S /Q 2>nul + + echo ===== Verifying checkout ===== + if exist Dockerfile.windows ( + echo SUCCESS: Repository checked out, Dockerfile.windows found + ) else ( + echo ERROR: Dockerfile.windows not found + dir /B + exit /b 1 + ) + env: + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + shell: cmd + + - name: Debug - Verify files + run: | + echo Working directory: + cd + echo. + echo Dockerfiles found: + dir Dockerfile* /B 2>nul || echo No Dockerfiles found + shell: cmd + continue-on-error: true + + - name: Debug - Verify secret + run: | + if "${{ secrets.REGISTRY_TOKEN }}"=="" ( + echo ERROR: REGISTRY_TOKEN not configured! + exit /b 1 + ) else ( + echo REGISTRY_TOKEN is configured + ) + shell: cmd + + - name: Log in to Gitea Container Registry + run: echo ${{ secrets.REGISTRY_TOKEN }} | docker login ${{ env.REGISTRY }} -u alessio --password-stdin + shell: cmd + + - name: Build and push Windows Docker image + run: | + set IMAGE_LOWER=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + set BRANCH=${{ github.ref_name }} + set SHA=${{ github.sha }} + set SHORT_SHA=%SHA:~0,7% + + REM Determine tags based on branch + set TAGS= + if "%BRANCH%"=="main" ( + set TAGS=%IMAGE_LOWER%:latest-windows + set TAGS=%TAGS% %IMAGE_LOWER%:main-windows-%SHORT_SHA% + ) + if "%BRANCH%"=="development" ( + set TAGS=%IMAGE_LOWER%:latest-windows + set TAGS=%TAGS% %IMAGE_LOWER%:development-latest-windows + set TAGS=%TAGS% %IMAGE_LOWER%:development-windows-%SHORT_SHA% + ) + if "%BRANCH%"=="staging" ( + set TAGS=%IMAGE_LOWER%:staging-latest-windows + set TAGS=%TAGS% %IMAGE_LOWER%:staging-windows-%SHORT_SHA% + ) + + echo Building Windows Docker image... + docker build -t temp-windows -f Dockerfile.windows . + if errorlevel 1 ( + echo Build failed! + exit /b 1 + ) + + echo Tagging and pushing images... + for %%t in (%TAGS%) do ( + echo Tagging: %%t + docker tag temp-windows %%t + echo Pushing: %%t + docker push %%t + ) + + echo Cleaning up temporary image... + docker rmi temp-windows + + echo Windows build completed successfully! + shell: cmd + + create-manifest: + name: Create Multi-Platform Manifest + runs-on: ubuntu-latest + needs: [build-linux, build-windows] + permissions: + contents: read + packages: write + + steps: + - name: Log in to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: alessio + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Create and push manifest for main branch + if: github.ref == 'refs/heads/main' + run: | + IMAGE_LOWER=$(echo "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]') + docker buildx imagetools create -t ${IMAGE_LOWER}:latest \ + ${IMAGE_LOWER}:latest \ + ${IMAGE_LOWER}:latest-windows + + - name: Create and push manifest for development branch + if: github.ref == 'refs/heads/development' + run: | + IMAGE_LOWER=$(echo "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]') + docker buildx imagetools create -t ${IMAGE_LOWER}:latest \ + ${IMAGE_LOWER}:latest \ + ${IMAGE_LOWER}:latest-windows + docker buildx imagetools create -t ${IMAGE_LOWER}:development-latest \ + ${IMAGE_LOWER}:development-latest \ + ${IMAGE_LOWER}:development-latest-windows + + - name: Create and push manifest for dev branch + if: github.ref == 'refs/heads/dev' + run: | + IMAGE_LOWER=$(echo "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]') + docker buildx imagetools create -t ${IMAGE_LOWER}:dev-latest \ + ${IMAGE_LOWER}:dev-latest \ + ${IMAGE_LOWER}:dev-latest-windows + + - name: Create and push manifest for staging branch + if: github.ref == 'refs/heads/staging' + run: | + IMAGE_LOWER=$(echo "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]') + docker buildx imagetools create -t ${IMAGE_LOWER}:staging-latest \ + ${IMAGE_LOWER}:staging-latest \ + ${IMAGE_LOWER}:staging-latest-windows diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..52b98a0 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,468 @@ +# Data-Coupler - GitHub Copilot Instructions + +## 📋 Panoramica del Progetto + +**Data-Coupler** è una soluzione enterprise di integrazione dati sviluppata in .NET 9.0 con architettura Blazor Server. Il sistema facilita il trasferimento e la sincronizzazione di dati tra diverse tipologie di sorgenti: database relazionali, REST API, file Excel/CSV con funzionalità avanzate di mapping, scheduling e gestione associazioni. + +## 🏗️ Architettura del Sistema + +### Progetti Principali + +1. **CredentialManager** - Libreria per gestione sicura delle credenziali + - Crittografia dei dati sensibili con Data Protection API + - Persistenza su database SQLite + - Supporto per credenziali database e REST API + - Gestione profili Data Coupler e schedulazioni + +2. **DataConnection** - Libreria per connessioni dati + - **Database supportati**: SQL Server, MySQL, PostgreSQL, Oracle, SQLite, IBM DB2, SAP HANA + - **REST API**: Generic REST, Salesforce, SAP Business One Service Layer + - Factory pattern per gestione provider + - Operazioni batch e parallel processing + +3. **Data_Coupler** - Applicazione Blazor Server + - Interfaccia web per gestione credenziali e profili + - Sistema di trasferimento dati con mapping intelligente + - Schedulazione avanzata con background services + - Sistema di backup e ripristino completo + +4. **Components** - Libreria componenti UI riutilizzabili + - Componenti Blazor per gestione profili + - Selettori e editor configurazioni + - UI responsive con Bootstrap 5 + +## 🎯 Feature Implementate + +### 1. Gestione Credenziali Sicura + +#### Caratteristiche: +- **Crittografia**: Password e chiavi API crittografate con `IDataProtectionProvider` +- **Multi-piattaforma**: Supporto per database eterogenei e REST API +- **Test Connessioni**: Validazione credenziali prima del salvataggio +- **CRUD Completo**: Interfaccia web per gestione completa + +#### File Chiave: +- `CredentialManager/Services/ICredentialService.cs` +- `CredentialManager/Services/EncryptionService.cs` +- `Data_Coupler/Pages/CredentialManagement.razor` + +### 2. Sistema di Profili Data Coupler + +#### Caratteristiche: +- **Profili Riutilizzabili**: Configurazioni salvate per trasferimenti ricorrenti +- **Mapping Campi**: Sistema avanzato di mappatura tra sorgente e destinazione +- **Sorgenti Multiple**: Database, REST API, File (Excel/CSV) +- **Gestione Completa**: CRUD con validazione e test + +#### Componenti: +- **Sorgente**: Tipo (Database/REST/File), credenziale, tabella/endpoint/percorso file +- **Destinazione**: Tipo, credenziale, tabella/endpoint +- **Mapping**: Corrispondenza campi sorgente → destinazione +- **Chiave Sorgente**: Campo identificativo univoco per tracking + +#### File Chiave: +- `CredentialManager/Models/DataCouplerProfile.cs` +- `CredentialManager/Services/DataCouplerProfileService.cs` +- `Data_Coupler/Pages/ProfilesManagement.razor` + +### 3. Sistema di Associazioni Record (Key Associations) + +#### Caratteristiche: +- **Tracciamento ID**: Memorizza relazione tra chiavi sorgente e ID destinazione +- **Prevenzione Duplicati**: Verifica esistenza prima di creare nuovi record +- **Pre-Discovery**: Cerca record esistenti nella destinazione prima della creazione +- **Hash Comparison**: Rileva modifiche dati tramite hash SHA256 +- **Validazione Intelligente**: Verifica esistenza ID destinazione, cleanup automatico + +#### Flusso Operativo: +1. Estrazione record da sorgente +2. Verifica associazione esistente in locale +3. **PRE-DISCOVERY**: Se non esiste, cerca nella destinazione tramite chiave +4. Se trovato → Crea associazione + Aggiorna record +5. Se non trovato → Crea nuovo record + Associazione +6. Calcola hash dati per tracking modifiche future + +#### File Chiave: +- `CredentialManager/Models/KeyAssociation.cs` +- `CredentialManager/Services/KeyAssociationService.cs` +- `Data_Coupler/Pages/KeyAssociations.razor` +- `Data_Coupler/Services/AssociationService.cs` + +#### Gestione Avanzata: +- **Pulizia Selettiva**: Elimina associazioni per sorgente/destinazione specifica +- **Pulizia Totale**: Reset completo sistema associazioni +- **Validazione**: Trova e pulisce associazioni con ID non più validi +- **Export/Import**: CSV per backup e migrazione dati + +### 4. Trasferimento Dati Avanzato + +#### Caratteristiche Standard: +- **Mapping Automatico**: Trasformazione campi sorgente → destinazione +- **Batch Processing**: Elaborazione ottimizzata per grandi volumi +- **Error Handling**: Gestione errori robusta con retry logic +- **Logging Dettagliato**: Tracciamento completo operazioni + +#### Salesforce Composite API: +- **Batch Operations**: Fino a 25 operazioni per request (limite Salesforce) +- **Parallel Processing**: Elaborazione parallela batch multipli +- **Performance**: 10-25x più veloce per grandi dataset +- **Riduzione API Calls**: 60-90% in meno chiamate + +#### Metodi Batch Implementati: +- `BatchExecuteQueriesAsync`: Esecuzione parallela multiple query SOQL +- `BatchFindEntitiesByKeysAsync`: Ricerca batch entità con diverse chiavi +- `BatchGetEntitiesByIdsAsync`: Recupero batch tramite ID (max 200 per query) +- `ExtractAllEntitiesAsync`: Estrazione completa con paginazione automatica +- `ExtractEntitiesParallelAsync`: Estrazione parallela con deduplicazione +- `ExtractLargeDatasetAsync`: Estrattore intelligente con auto-detect strategia +- `ExtractRecentlyModifiedAsync`: Sincronizzazione incrementale + +#### File Chiave: +- `Data_Coupler/Pages/DataCoupler.razor.cs` +- `DataConnection/REST/Implementations/SalesforceServiceClient.cs` +- `Data_Coupler/Services/ScheduledProfileExecutionService.cs` + +### 5. Sistema di Schedulazione Avanzata + +#### Tipi di Schedulazione: +1. **Una Volta (Once)**: Esecuzione singola a data/ora specifica +2. **Giornaliera (Daily)**: Ogni giorno a orario specifico +3. **Settimanale (Weekly)**: Ogni settimana in giorno specifico +4. **Mensile (Monthly)**: Ogni mese in giorno specifico +5. **A Intervalli (Interval)**: Ricorrente ogni N unità di tempo + - Secondi, Minuti, Ore, Giorni, Settimane, Mesi + - Esempio: Ogni 5 minuti, ogni 2 ore, ogni 30 secondi + +#### Componenti Sistema: +- **ProfileSchedule**: Modello dati schedulazione +- **ScheduledExecutionBackgroundService**: Background service per esecuzione automatica +- **ScheduledProfileExecutionService**: Logica completa trasferimento schedulato +- **ProfileScheduleService**: CRUD e gestione schedule + +#### Funzionalità: +- **Esecuzione Automatica**: Background service sempre attivo +- **Gestione Stato**: Tracciamento esecuzioni (success, failed, running) +- **Storico Esecuzioni**: Log completo con timestamp, record processati, errori +- **Pausa/Riprendi**: Controllo dinamico schedulazioni +- **Override Database**: Possibilità di sovrascrivere sorgente/destinazione +- **Deletion Sync Configurabile**: Opzione per abilitare sincronizzazione eliminazioni (disabilitata di default) + +#### File Chiave: +- `CredentialManager/Models/ProfileSchedule.cs` +- `CredentialManager/Services/ProfileScheduleService.cs` +- `Data_Coupler/BackgroundServices/ScheduledExecutionBackgroundService.cs` +- `Data_Coupler/Pages/Scheduling.razor` +- `Data_Coupler/Pages/SchedulingHistory.razor` + +### 6. Sistema di Backup e Ripristino + +#### Funzionalità Export: +- **Configurazione Flessibile**: Export selettivo per componente +- **Componenti Supportati**: + - Profili Data Coupler + - Credenziali (senza dati sensibili) + - Associazioni Chiavi + - Schedulazioni +- **Formato JSON**: Leggibile e versionato +- **Metadati**: Timestamp, utente, descrizione, componenti inclusi + +#### Funzionalità Import: +- **Validazione Rigorosa**: Controllo formato, versione, integrità dati +- **Modalità Ripristino**: + - **Overwrite**: Sostituisce dati esistenti + - **Merge**: Integra con dati esistenti + - **Preview**: Mostra anteprima senza eseguire +- **Transazioni Sicure**: Rollback automatico in caso di errori + +#### Sicurezza: +- **Esclusione Dati Sensibili**: Password e API keys non inclusi in backup +- **Crittografia Opzionale**: Backup crittografati per sicurezza extra +- **Audit Logging**: Tracciamento completo operazioni backup/restore + +#### File Chiave: +- `Data_Coupler/Services/BackupService.cs` +- `Data_Coupler/Models/BackupModels.cs` +- `Data_Coupler/Pages/SettingsPage.razor` + +### 7. Sincronizzazione Eliminazioni (Deletion Sync) + +#### Caratteristiche: +- **Rilevamento Eliminazioni**: Identifica record eliminati da sorgente +- **Sincronizzazione Automatica**: Elimina corrispondenti in destinazione +- **Gestione Associazioni**: Aggiorna/elimina associazioni correlate +- **Modalità Sicura**: Preview eliminazioni prima dell'esecuzione +- **Logging Completo**: Traccia tutte le operazioni di eliminazione +- **Configurazione Granulare**: + - **Disabilitata** completamente nei trasferimenti manuali (DataCoupler.razor) + - **Configurabile** nelle schedulazioni tramite flag `EnableDeletionSync` + - **Default: false** per massima sicurezza + - Warning esplicito nell'UI per operazioni critiche + +#### Sicurezza: +- La funzionalità è **disabilitata di default** per evitare eliminazioni accidentali +- Disponibile **solo per le schedulazioni** con configurazione esplicita +- L'utente deve attivamente abilitare la funzione con piena consapevolezza +- Logging completo di tutte le operazioni di eliminazione per audit trail + +#### File Chiave: +- `Data_Coupler/Services/DeletionSyncService.cs` +- `Data_Coupler/Services/ScheduledProfileExecutionService.cs` +- `CredentialManager/Models/ProfileSchedule.cs` (campo `EnableDeletionSync`) +- `Data_Coupler/Pages/Scheduling.razor` (UI configurazione) + +### 8. Sistema di Autenticazione + +#### Caratteristiche: +- **Login Protetto**: Autenticazione via credenziali o token +- **Session Management**: Gestione sessioni utente +- **Password Sicure**: Hashing con algoritmi moderni +- **Remember Me**: Persistenza login opzionale + +#### File Chiave: +- `Data_Coupler/Services/AuthenticationService.cs` +- `Data_Coupler/Pages/Login.razor` +- `Data_Coupler/Middleware/AuthenticationMiddleware.cs` + +### 9. Interfaccia Utente Avanzata + +#### Pagine Principali: +- **Dashboard** (`Index.razor`): Panoramica sistema e quick actions +- **Data Coupler** (`DataCoupler.razor`): Interfaccia principale trasferimento dati +- **Gestione Profili** (`ProfilesManagement.razor`): CRUD profili completo +- **Schedulazione** (`Scheduling.razor`): Gestione schedule con UI intuitiva +- **Storico Schedulazioni** (`SchedulingHistory.razor`): Visualizzazione esecuzioni +- **Associazioni** (`KeyAssociations.razor`): Gestione completa associazioni +- **Credenziali** (`CredentialManagement.razor`): CRUD credenziali +- **Impostazioni** (`SettingsPage.razor`): Backup, sistema, sicurezza, manutenzione + +#### Componenti UI: +- **ProfileSelector**: Selezione e caricamento profili +- **ProfileSaver**: Salvataggio profili con validazione +- **ProfileManagement**: CRUD profili completo +- **Toast Notifications**: Feedback utente in tempo reale +- **Modal Dialogs**: Conferme operazioni critiche +- **Progress Indicators**: Feedback operazioni lunghe + +#### Design: +- **Bootstrap 5**: Framework CSS responsive +- **Font Awesome**: Iconografia consistente +- **Dark/Light Mode**: Temi personalizzabili +- **Mobile Responsive**: Ottimizzato per dispositivi mobili + +### 10. Health Checks e Monitoraggio + +#### Caratteristiche: +- **Health Checks**: Endpoint per monitoraggio stato applicazione +- **Database Health**: Verifica connessione database +- **Background Services**: Stato background services +- **Memory Monitoring**: Tracciamento utilizzo memoria +- **Performance Metrics**: Statistiche performance sistema + +#### File Chiave: +- `Data_Coupler/HealthChecks/DatabaseHealthCheck.cs` +- `Data_Coupler/HealthChecks/BackgroundServiceHealthCheck.cs` + +## 🔐 Sicurezza + +### Gestione Credenziali: +- **Data Protection API**: Crittografia con chiavi managed da .NET +- **Key Rotation**: Supporto rotazione chiavi automatica +- **Secure Storage**: Persistenza sicura su SQLite + +### Input Validation: +- **SQL Injection Prevention**: Query parametrizzate ovunque +- **XSS Protection**: Sanitizzazione input utente +- **Whitelist Validation**: Validazione nomi tabelle/campi + +### Comunicazioni Sicure: +- **HTTPS**: Forzato in produzione +- **Certificate Validation**: Validazione certificati SSL/TLS +- **Secure Headers**: Security headers HTTP + +### Audit Logging: +- **Tracciamento Accessi**: Log accessi credenziali +- **Operazioni Sensibili**: Log modifiche configurazioni +- **Security Events**: Alert eventi sicurezza + +## 🛠️ Convenzioni di Codice + +### Naming: +- **Classes/Interfaces**: PascalCase (es. `DataConnectionFactory`) +- **Methods**: PascalCase (es. `GetDataAsync`) +- **Properties**: PascalCase (es. `ConnectionString`) +- **Private Fields**: camelCase con underscore (es. `_logger`) +- **Local Variables**: camelCase (es. `connectionName`) + +### Async/Await: +- Tutti i metodi I/O sono asincroni +- Naming convention: `*Async` suffix +- Uso di `CancellationToken` per operazioni cancellabili +- **MAI** usare `.Result` o `.Wait()` - rischio deadlock + +### Dependency Injection: +- **Singleton**: Servizi stateless (es. `IEncryptionService`) +- **Scoped**: Servizi con stato per request (es. `IDataConnectionFactory`) +- **Transient**: Servizi leggeri (es. validators) + +### Error Handling: +- Try-catch con logging specifico +- Messaggi utente user-friendly +- Dettagli tecnici nei log, non in UI +- Rollback transazioni in caso di errore + +## 📝 Pattern Architetturali Utilizzati + +### 1. Repository Pattern: +- Astrazione accesso dati +- `*Service` interfacce per business logic +- `*Repository` per accesso database + +### 2. Factory Pattern: +- `DataConnectionFactory`: Creazione connessioni database/REST +- `RestServiceClientFactory`: Creazione client REST specifici + +### 3. Strategy Pattern: +- Diversi provider database (SQL Server, MySQL, PostgreSQL, etc.) +- Diversi client REST (Generic, Salesforce, SAP B1) + +### 4. Observer Pattern: +- Background services per schedulazioni +- Event-driven processing + +## 🧪 Testing + +### Unit Tests: +- Copertura servizi core +- Mock di dipendenze esterne +- Test naming: `MethodName_Scenario_ExpectedBehavior` + +### Integration Tests: +- Test connessioni database reali +- Test API REST con server di test +- Test end-to-end UI con Playwright + +## 📦 Deployment + +### Docker Support: +- `Dockerfile`: Immagine Linux +- `Dockerfile.windows`: Immagine Windows +- `docker-compose.yml`: Orchestrazione multi-container +- Health checks integrati + +### Windows Service: +- Deploy come Windows Service +- Auto-start configurabile +- Logging Windows Event Log + +### Azure Deployment: +- App Service support +- Container Apps support +- Key Vault integration per secrets + +## 🔄 Workflow di Sviluppo + +### Branching Strategy: +- `main`: Produzione stabile +- `development`: Sviluppo attivo (pubblica su `latest` tag) +- `feature/*`: Nuove feature +- `hotfix/*`: Fix urgenti + +### CI/CD Pipeline: + +#### GitHub Actions (`.github/workflows/docker-build.yml`) +- **Branch `main`**: Pubblica immagini Docker con tag `latest` +- **Branch `development`**: Pubblica immagini Docker con tag `latest` e `development-latest` +- **Branch `staging`**: Pubblica immagini Docker con tag `staging-latest` +- **Ogni commit**: Crea tag con SHA e timestamp per tracciabilità +- **Registry**: GitHub Container Registry (`ghcr.io`) + +#### Gitea Actions (`.gitea/workflows/docker-build.yml`) +- **Stessa configurazione** di GitHub Actions +- **Registry**: Gitea Container Registry (`gitea.home-nas-ds.org`) +- **Supporto**: Istanza Gitea self-hosted con registry abilitato +- **Setup**: Richiede secret `REGISTRY_TOKEN` con permessi `write:packages` +- **Documentazione**: `.gitea/workflows/README.md` + +**Note sui Tag Docker**: +- `latest`: Condiviso tra `main` e `development` per garantire accesso alle ultime funzionalità +- `development-latest`: Specifico per il branch `development`, utile per distinguere le versioni in sviluppo +- `staging-latest`: Dedicato al branch `staging` per test pre-produzione +- Disponibile su **entrambi** i registry (GitHub e Gitea) + +### Commit Messages: +- Formato: `[Tipo] Descrizione breve` +- Tipi: `[Feature]`, `[Fix]`, `[Refactor]`, `[Docs]`, `[Test]` +- Esempio: `[Feature] Implementato sistema schedulazione avanzata` + +### Code Review: +- Ogni PR richiede review +- Checklist: code style, tests, documentation +- CI/CD automatico + +## 📚 Documentazione Correlata + +### File di Documentazione nel Progetto: +- **AGENTS.md**: Guida completa per AI agents e Copilot +- **ADVANCED_SCHEDULING_SYSTEM.md**: Sistema schedulazione dettagliato +- **GESTIONE_ASSOCIAZIONI_AVANZATA.md**: Sistema associazioni completo +- **SALESFORCE_BATCH_EXTRACTION_IMPROVEMENTS.md**: Batch extraction Salesforce +- **PRE_DISCOVERY_SYSTEM.md**: Sistema pre-discovery associazioni +- **DELETION_SYNC_IMPLEMENTATION.md**: Sincronizzazione eliminazioni +- **DOCKER_DEPLOYMENT.md**: Guida deployment Docker +- **WINDOWS_SERVICE_DEPLOYMENT.md**: Deploy come Windows Service +- **.gitea/workflows/README.md**: Configurazione Gitea Actions + +## 🎓 Best Practices per AI Assistants + +### Quando Modificare Codice: +1. **Leggere Documentazione**: Consultare file MD correlati +2. **Analizzare Pattern**: Seguire pattern esistenti nel codebase +3. **Validare Input**: Sempre validare parametri +4. **Error Handling**: Gestire tutti i possibili errori +5. **Logging**: Aggiungere log appropriati +6. **Testing**: Creare/aggiornare test correlati +7. **Documentazione**: Aggiornare XML comments e MD files + +### Quando Creare Nuove Feature: +1. **Progettare Prima**: Pianificare architettura +2. **Seguire Convenzioni**: Rispettare code style +3. **Dependency Injection**: Registrare nuovi servizi in `Program.cs` +4. **Database Changes**: Creare migration Entity Framework +5. **UI Consistency**: Seguire design pattern esistente +6. **Documentare**: Creare/aggiornare documentazione + +### Red Flags da Evitare: +- ❌ Hardcoded credentials o secrets +- ❌ SQL injection vulnerabilities +- ❌ Blocking calls in async methods +- ❌ Catch generico senza logging +- ❌ Modifiche database senza migration +- ❌ Breaking changes senza versioning +- ❌ Codice duplicato invece di refactoring + +## 🚀 Roadmap Futura + +### Feature in Pianificazione: +- [ ] Supporto file Excel/CSV avanzato +- [ ] Sistema di notifiche (email, webhook) +- [ ] Dashboard analytics avanzato +- [ ] Multi-tenant support +- [ ] API REST pubblica +- [ ] Plugin system per connectors custom +- [ ] Machine learning per mapping suggeriti +- [ ] Real-time data sync + +### Miglioramenti Tecnici: +- [ ] Migrazione a .NET 10 (quando disponibile) +- [ ] Caching distribuito (Redis) +- [ ] Message queue (RabbitMQ/Azure Service Bus) +- [ ] Elasticsearch per log search +- [ ] Prometheus/Grafana per monitoring + +--- + +**Versione**: 2.0 +**Ultimo Aggiornamento**: 22 Gennaio 2026 +**Framework**: .NET 9.0 +**Sviluppatore**: Alessio Dalsanto +**Repository**: https://github.com/AlessioDalsi/Data-Coupler diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 3f48edc..0fbbbe6 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -4,7 +4,7 @@ on: push: branches: - main - - dev + - development - staging workflow_dispatch: inputs: @@ -50,6 +50,8 @@ jobs: tags: | # Tag based on branch type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/development' }} + type=raw,value=development-latest,enable=${{ github.ref == 'refs/heads/development' }} type=raw,value=dev-latest,enable=${{ github.ref == 'refs/heads/dev' }} type=raw,value=staging-latest,enable=${{ github.ref == 'refs/heads/staging' }} # Tag with commit sha @@ -107,6 +109,8 @@ jobs: tags: | # Tag based on branch with windows suffix type=raw,value=latest-windows,enable=${{ github.ref == 'refs/heads/main' }} + type=raw,value=latest-windows,enable=${{ github.ref == 'refs/heads/development' }} + type=raw,value=development-latest-windows,enable=${{ github.ref == 'refs/heads/development' }} type=raw,value=dev-latest-windows,enable=${{ github.ref == 'refs/heads/dev' }} type=raw,value=staging-latest-windows,enable=${{ github.ref == 'refs/heads/staging' }} # Tag with commit sha @@ -165,6 +169,17 @@ jobs: ${IMAGE_LOWER}:latest \ ${IMAGE_LOWER}:latest-windows + - name: Create and push manifest for development branch + if: github.ref == 'refs/heads/development' + run: | + IMAGE_LOWER=$(echo "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]') + docker buildx imagetools create -t ${IMAGE_LOWER}:latest \ + ${IMAGE_LOWER}:latest \ + ${IMAGE_LOWER}:latest-windows + docker buildx imagetools create -t ${IMAGE_LOWER}:development-latest \ + ${IMAGE_LOWER}:development-latest \ + ${IMAGE_LOWER}:development-latest-windows + - name: Create and push manifest for dev branch if: github.ref == 'refs/heads/dev' run: | diff --git a/CredentialManager/Migrations/20260123104841_AddEnableDeletionSyncToProfileSchedule.Designer.cs b/CredentialManager/Migrations/20260123104841_AddEnableDeletionSyncToProfileSchedule.Designer.cs new file mode 100644 index 0000000..897a8d6 --- /dev/null +++ b/CredentialManager/Migrations/20260123104841_AddEnableDeletionSyncToProfileSchedule.Designer.cs @@ -0,0 +1,585 @@ +// +using System; +using CredentialManager.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CredentialManager.Migrations +{ + [DbContext(typeof(CredentialDbContext))] + [Migration("20260123104841_AddEnableDeletionSyncToProfileSchedule")] + partial class AddEnableDeletionSyncToProfileSchedule + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("CredentialManager.Models.CredentialEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalParameters") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CommandTimeout") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(30); + + b.Property("ConnectionString") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DatabaseName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DatabaseType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("EncryptedApiKey") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EncryptedAuthToken") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EncryptedPassword") + .HasColumnType("TEXT"); + + b.Property("Headers") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Host") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IgnoreSslErrors") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("RestServiceType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TimeoutSeconds") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(100); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Username") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DatabaseType"); + + b.HasIndex("IsActive"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("Type"); + + b.ToTable("Credentials", (string)null); + }); + + modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DeletionAction") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("DeletionMarkField") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DeletionMarkValue") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DestinationCredentialId") + .HasColumnType("INTEGER"); + + b.Property("DestinationEndpoint") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DestinationSchema") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationTable") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("FieldMappingJson") + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("LastUsedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SourceCredentialId") + .HasColumnType("INTEGER"); + + b.Property("SourceCustomQuery") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("SourceDatabaseName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourceFilePath") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SourceKeyField") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourceSchema") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourceTable") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourceType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("SyncDeletions") + .HasColumnType("INTEGER"); + + b.Property("UseRecordAssociations") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("DestinationCredentialId"); + + b.HasIndex("DestinationType"); + + b.HasIndex("IsActive"); + + b.HasIndex("LastUsedAt"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("SourceCredentialId"); + + b.HasIndex("SourceType"); + + b.ToTable("DataCouplerProfiles", (string)null); + }); + + modelBuilder.Entity("CredentialManager.Models.KeyAssociation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalInfo") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Data_Hash") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("DeletionSynced") + .HasColumnType("INTEGER"); + + b.Property("DeletionSyncedAt") + .HasColumnType("TEXT"); + + b.Property("DestinationEntity") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationKeyField") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("IsSourceDeleted") + .HasColumnType("INTEGER"); + + b.Property("KeyValue") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastVerifiedAt") + .HasColumnType("TEXT"); + + b.Property("MappedDestinationField") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RestCredentialName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SourceKeyField") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourcesInfo") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("DestinationEntity"); + + b.HasIndex("IsActive"); + + b.HasIndex("KeyValue") + .HasDatabaseName("IX_KeyAssociations_KeyValue"); + + b.HasIndex("LastVerifiedAt"); + + b.HasIndex("RestCredentialName"); + + b.HasIndex("KeyValue", "DestinationEntity", "RestCredentialName") + .IsUnique() + .HasDatabaseName("IX_KeyAssociations_Unique"); + + b.ToTable("KeyAssociations", (string)null); + }); + + modelBuilder.Entity("CredentialManager.Models.ProfileSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DailyTime") + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("DayOfMonth") + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DestinationDatabaseOverride") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("EnableDeletionSync") + .HasColumnType("INTEGER"); + + b.Property("ExecutionCount") + .HasColumnType("INTEGER"); + + b.Property("IntervalUnit") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("IntervalValue") + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("LastExecutionMessage") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("LastExecutionRecordCount") + .HasColumnType("INTEGER"); + + b.Property("LastExecutionStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("LastExecutionTime") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("NextExecutionTime") + .HasColumnType("TEXT"); + + b.Property("ProfileId") + .HasColumnType("INTEGER"); + + b.Property("ScheduleType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("ScheduledDateTime") + .HasColumnType("TEXT"); + + b.Property("SourceDatabaseOverride") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId"); + + b.ToTable("ProfileSchedules"); + }); + + modelBuilder.Entity("CredentialManager.Models.ScheduleExecutionHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalInfo") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DestinationInfo") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DestinationType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("ErrorDetails") + .HasMaxLength(5000) + .HasColumnType("TEXT"); + + b.Property("Message") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("ProfileId") + .HasColumnType("INTEGER"); + + b.Property("ProfileName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RecordsProcessed") + .HasColumnType("INTEGER"); + + b.Property("RecordsWithErrors") + .HasColumnType("INTEGER"); + + b.Property("ScheduleId") + .HasColumnType("INTEGER"); + + b.Property("SourceInfo") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SourceType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("TriggerType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("TriggeredBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ScheduleId"); + + b.HasIndex("StartTime"); + + b.HasIndex("Status"); + + b.HasIndex("TriggerType"); + + b.ToTable("ScheduleExecutionHistories", (string)null); + }); + + modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b => + { + b.HasOne("CredentialManager.Models.CredentialEntity", "DestinationCredential") + .WithMany() + .HasForeignKey("DestinationCredentialId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("CredentialManager.Models.CredentialEntity", "SourceCredential") + .WithMany() + .HasForeignKey("SourceCredentialId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("DestinationCredential"); + + b.Navigation("SourceCredential"); + }); + + modelBuilder.Entity("CredentialManager.Models.ProfileSchedule", b => + { + b.HasOne("CredentialManager.Models.DataCouplerProfile", "Profile") + .WithMany() + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("CredentialManager.Models.ScheduleExecutionHistory", b => + { + b.HasOne("CredentialManager.Models.ProfileSchedule", "Schedule") + .WithMany() + .HasForeignKey("ScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Schedule"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CredentialManager/Migrations/20260123104841_AddEnableDeletionSyncToProfileSchedule.cs b/CredentialManager/Migrations/20260123104841_AddEnableDeletionSyncToProfileSchedule.cs new file mode 100644 index 0000000..d202b37 --- /dev/null +++ b/CredentialManager/Migrations/20260123104841_AddEnableDeletionSyncToProfileSchedule.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CredentialManager.Migrations +{ + /// + public partial class AddEnableDeletionSyncToProfileSchedule : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "EnableDeletionSync", + table: "ProfileSchedules", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "EnableDeletionSync", + table: "ProfileSchedules"); + } + } +} diff --git a/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs b/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs index 885c024..64a742e 100644 --- a/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs +++ b/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs @@ -382,6 +382,9 @@ namespace CredentialManager.Migrations .HasMaxLength(100) .HasColumnType("TEXT"); + b.Property("EnableDeletionSync") + .HasColumnType("INTEGER"); + b.Property("ExecutionCount") .HasColumnType("INTEGER"); diff --git a/CredentialManager/Models/ProfileSchedule.cs b/CredentialManager/Models/ProfileSchedule.cs index 9de9e3c..593931f 100644 --- a/CredentialManager/Models/ProfileSchedule.cs +++ b/CredentialManager/Models/ProfileSchedule.cs @@ -70,6 +70,9 @@ public class ProfileSchedule [MaxLength(100)] public string? DestinationDatabaseOverride { get; set; } + // Configurazione sincronizzazione eliminazioni (default: disabilitata) + public bool EnableDeletionSync { get; set; } = false; + // Metadati [MaxLength(100)] public string? CreatedBy { get; set; } diff --git a/CredentialManager/design_time_temp.db b/CredentialManager/design_time_temp.db index 589aa35..0482876 100644 Binary files a/CredentialManager/design_time_temp.db and b/CredentialManager/design_time_temp.db differ diff --git a/Data_Coupler/Models/BackupModels.cs b/Data_Coupler/Models/BackupModels.cs index 4eeb651..423ef01 100644 --- a/Data_Coupler/Models/BackupModels.cs +++ b/Data_Coupler/Models/BackupModels.cs @@ -289,6 +289,9 @@ public class ProfileScheduleBackup [JsonPropertyName("createdBy")] public string? CreatedBy { get; set; } + + [JsonPropertyName("enableDeletionSync")] + public bool EnableDeletionSync { get; set; } = false; } /// diff --git a/Data_Coupler/Pages/DataCoupler.razor.cs b/Data_Coupler/Pages/DataCoupler.razor.cs index 6cf9b76..ff532e7 100644 --- a/Data_Coupler/Pages/DataCoupler.razor.cs +++ b/Data_Coupler/Pages/DataCoupler.razor.cs @@ -1496,8 +1496,10 @@ public partial class DataCoupler : ComponentBase recordNumber++; } - // 3.5 Sincronizza le cancellazioni (se abilitato) + // 3.5 Sincronizzazione cancellazioni (DISABILITATA per trasferimenti manuali) + // Questa funzionalità è disponibile solo per le schedulazioni con configurazione esplicita int deletedCount = 0; + /* DELETION SYNC DISABILITATA PER TRASFERIMENTI MANUALI if (useRecordAssociations && !string.IsNullOrEmpty(sourceKeyField)) { try @@ -1570,6 +1572,7 @@ public partial class DataCoupler : ComponentBase }); } } + */ // 4. Mostra risultati if (errorCount == 0) diff --git a/Data_Coupler/Pages/Scheduling.razor b/Data_Coupler/Pages/Scheduling.razor index d780b72..d6bd0b3 100644 --- a/Data_Coupler/Pages/Scheduling.razor +++ b/Data_Coupler/Pages/Scheduling.razor @@ -336,6 +336,30 @@ +
+
+
+ Opzioni Avanzate +
+
+
+
+ + +
+
+ + + Attenzione: Se abilitata, i record eliminati dalla sorgente saranno automaticamente eliminati anche dalla destinazione durante l'esecuzione schedulata. + Questa opzione è disabilitata di default per motivi di sicurezza. + Usare con cautela! + +
+
+
+
Task ExecuteProfileAsync(int profileId); + + /// + /// Esegue un profilo Data Coupler specificato dall'ID con configurazione sincronizzazione eliminazioni + /// + /// ID del profilo da eseguire + /// Se true, sincronizza le eliminazioni dalla sorgente alla destinazione + Task ExecuteProfileAsync(int profileId, bool enableDeletionSync); } \ No newline at end of file diff --git a/Data_Coupler/Services/ScheduledExecutionBackgroundService.cs b/Data_Coupler/Services/ScheduledExecutionBackgroundService.cs index d0cfbc0..1b5ccaf 100644 --- a/Data_Coupler/Services/ScheduledExecutionBackgroundService.cs +++ b/Data_Coupler/Services/ScheduledExecutionBackgroundService.cs @@ -69,11 +69,11 @@ public class ScheduledExecutionBackgroundService : BackgroundService { if (ShouldExecuteSchedule(schedule, currentTime)) { - _logger.LogInformation("Esecuzione schedulata per profilo: {ProfileName} (Schedule: {ScheduleName})", - schedule.Profile?.Name ?? "N/A", schedule.Name); + _logger.LogInformation("Esecuzione schedulata per profilo: {ProfileName} (Schedule: {ScheduleName}) - DeletionSync: {DeletionSync}", + schedule.Profile?.Name ?? "N/A", schedule.Name, schedule.EnableDeletionSync); - // Esegui il profilo - var result = await executionService.ExecuteProfileAsync(schedule.ProfileId); + // Esegui il profilo con il flag deletion sync dalla schedulazione + var result = await executionService.ExecuteProfileAsync(schedule.ProfileId, schedule.EnableDeletionSync); // Aggiorna la schedulazione await UpdateScheduleAfterExecution(scheduleService, schedule, currentTime, result.Success); diff --git a/Data_Coupler/Services/ScheduledProfileExecutionService.cs b/Data_Coupler/Services/ScheduledProfileExecutionService.cs index 2d51457..2413df4 100644 --- a/Data_Coupler/Services/ScheduledProfileExecutionService.cs +++ b/Data_Coupler/Services/ScheduledProfileExecutionService.cs @@ -28,6 +28,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic private readonly IDataConnectionCredentialService _dataConnectionCredentialService; private readonly IKeyAssociationService _keyAssociationService; private readonly IAssociationService _associationService; + private readonly IDeletionSyncService _deletionSyncService; private readonly ILogger _logger; public ScheduledProfileExecutionService( @@ -37,6 +38,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic IDataConnectionCredentialService dataConnectionCredentialService, IKeyAssociationService keyAssociationService, IAssociationService associationService, + IDeletionSyncService deletionSyncService, ILogger logger) { _profileService = profileService; @@ -45,6 +47,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic _dataConnectionCredentialService = dataConnectionCredentialService; _keyAssociationService = keyAssociationService; _associationService = associationService; + _deletionSyncService = deletionSyncService; _logger = logger; } @@ -52,6 +55,14 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic /// Esegue un profilo Data Coupler specificato dall'ID /// public async Task ExecuteProfileAsync(int profileId) + { + return await ExecuteProfileAsync(profileId, enableDeletionSync: false); + } + + /// + /// Esegue un profilo Data Coupler specificato dall'ID con configurazione sincronizzazione eliminazioni + /// + public async Task ExecuteProfileAsync(int profileId, bool enableDeletionSync) { var startTime = DateTime.UtcNow; var result = new ProfileExecutionResult @@ -61,7 +72,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic try { - _logger.LogInformation("Inizio esecuzione profilo schedulato ID: {ProfileId}", profileId); + _logger.LogInformation("Inizio esecuzione profilo schedulato ID: {ProfileId} - DeletionSync: {DeletionSync}", profileId, enableDeletionSync); // Carica il profilo var profile = await _profileService.GetProfileByIdAsync(profileId); @@ -78,7 +89,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic await _profileService.UpdateLastUsedAsync(profile.Id); // Esegue il trasferimento dati con la logica completa - var recordsTransferred = await ExecuteDataTransferAsync(profile); + var recordsTransferred = await ExecuteDataTransferAsync(profile, enableDeletionSync); result.Success = true; result.RecordsProcessed = recordsTransferred; @@ -106,10 +117,11 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic /// Metodo principale per l'esecuzione del trasferimento dati /// Implementa la stessa logica di StartDataTransferWithComposite /// - private async Task ExecuteDataTransferAsync(DataCouplerProfile profile) + private async Task ExecuteDataTransferAsync(DataCouplerProfile profile, bool enableDeletionSync = false) { _logger.LogInformation("=== INIZIO TRASFERIMENTO DATI SCHEDULATO ==="); - _logger.LogInformation("Esecuzione profilo: {ProfileName} (ID: {ProfileId})", profile.Name, profile.Id); + _logger.LogInformation("Esecuzione profilo: {ProfileName} (ID: {ProfileId}) - DeletionSync: {DeletionSync}", + profile.Name, profile.Id, enableDeletionSync); try { @@ -151,12 +163,12 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic if (useSalesforceComposite) { _logger.LogInformation("Utilizzo Salesforce Composite API per il trasferimento"); - return await ExecuteDataTransferWithCompositeAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings); + return await ExecuteDataTransferWithCompositeAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings, enableDeletionSync); } else { _logger.LogInformation("Utilizzo metodo trasferimento standard per il trasferimento"); - return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings); + return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings, enableDeletionSync); } } catch (Exception ex) @@ -389,9 +401,11 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic IRestServiceClient restClient, RestEntitySummary restEntity, RestApiCredential restCredential, - Dictionary fieldMappings) + Dictionary fieldMappings, + bool enableDeletionSync = false) { - _logger.LogInformation("Iniziando trasferimento dati standard per {RecordCount} record", sourceRecords.Count()); + _logger.LogInformation("Iniziando trasferimento dati standard per {RecordCount} record - DeletionSync: {DeletionSync}", + sourceRecords.Count(), enableDeletionSync); int successCount = 0; int errorCount = 0; @@ -454,6 +468,50 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic _logger.LogInformation("Trasferimento completato. Successi: {SuccessCount}, Errori: {ErrorCount}", successCount, errorCount); + // Sincronizzazione cancellazioni (se abilitata) + if (enableDeletionSync && profile.UseRecordAssociations && !string.IsNullOrEmpty(profile.SourceKeyField)) + { + try + { + _logger.LogInformation("SCHEDULED: Inizio sincronizzazione cancellazioni..."); + + // Estrai tutti i valori chiave presenti nella sorgente + var sourceKeyValues = sourceRecords + .Select(r => r.ContainsKey(profile.SourceKeyField) ? r[profile.SourceKeyField]?.ToString() : null) + .Where(k => !string.IsNullOrEmpty(k)) + .Cast() + .Distinct() + .ToList(); + + _logger.LogInformation("SCHEDULED: Trovati {Count} valori chiave nella sorgente", sourceKeyValues.Count); + + // Sincronizza le cancellazioni + var deletionOptions = new DeletionSyncOptions + { + Action = DeletionAction.Delete // Default: elimina fisicamente + }; + + var deletionResult = await _deletionSyncService.SyncDeletionsAsync( + sourceKeyValues, + restEntity.Name, + restCredential.Name, + restClient, + deletionOptions); + + if (deletionResult.DeletedRecordsDetected > 0) + { + _logger.LogInformation("SCHEDULED: Sincronizzazione cancellazioni completata - {Detected} rilevati, {Synced} sincronizzati, {Errors} errori", + deletionResult.DeletedRecordsDetected, + deletionResult.DeletedRecordsSynced, + deletionResult.SyncErrors); + } + } + catch (Exception delEx) + { + _logger.LogError(delEx, "SCHEDULED: Errore durante la sincronizzazione delle cancellazioni"); + } + } + return successCount; } @@ -467,15 +525,17 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic IRestServiceClient restClient, RestEntitySummary restEntity, RestApiCredential restCredential, - Dictionary fieldMappings) + Dictionary fieldMappings, + bool enableDeletionSync = false) { - _logger.LogInformation("Iniziando trasferimento dati COMPOSITE per {RecordCount} record", sourceRecords.Count()); + _logger.LogInformation("Iniziando trasferimento dati COMPOSITE per {RecordCount} record - DeletionSync: {DeletionSync}", + sourceRecords.Count(), enableDeletionSync); // Verifica che sia effettivamente un SalesforceServiceClient if (!(restClient is DataConnection.REST.Implementations.SalesforceServiceClient salesforceClient)) { _logger.LogWarning("Client REST non è SalesforceServiceClient, fallback al metodo standard"); - return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential, fieldMappings); + return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential, fieldMappings, enableDeletionSync); } try @@ -738,6 +798,50 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic _logger.LogInformation("COMPOSITE SCHEDULED: Trasferimento completato. Creazioni: {SuccessCount}, Aggiornamenti: {UpdatedCount}, Saltati: {SkippedCount}, Errori: {ErrorCount}", successCount, updatedCount, skippedCount, errorCount); + // Sincronizzazione cancellazioni (se abilitata) + if (enableDeletionSync && currentUseRecordAssociations && !string.IsNullOrEmpty(profile.SourceKeyField)) + { + try + { + _logger.LogInformation("COMPOSITE SCHEDULED: Inizio sincronizzazione cancellazioni..."); + + // Estrai tutti i valori chiave presenti nella sorgente + var sourceKeyValues = sourceRecords + .Select(r => r.ContainsKey(profile.SourceKeyField) ? r[profile.SourceKeyField]?.ToString() : null) + .Where(k => !string.IsNullOrEmpty(k)) + .Cast() + .Distinct() + .ToList(); + + _logger.LogInformation("COMPOSITE SCHEDULED: Trovati {Count} valori chiave nella sorgente", sourceKeyValues.Count); + + // Sincronizza le cancellazioni + var deletionOptions = new DeletionSyncOptions + { + Action = DeletionAction.Delete // Default: elimina fisicamente + }; + + var deletionResult = await _deletionSyncService.SyncDeletionsAsync( + sourceKeyValues, + currentEntityName, + currentCredentialName, + restClient, + deletionOptions); + + if (deletionResult.DeletedRecordsDetected > 0) + { + _logger.LogInformation("COMPOSITE SCHEDULED: Sincronizzazione cancellazioni completata - {Detected} rilevati, {Synced} sincronizzati, {Errors} errori", + deletionResult.DeletedRecordsDetected, + deletionResult.DeletedRecordsSynced, + deletionResult.SyncErrors); + } + } + catch (Exception delEx) + { + _logger.LogError(delEx, "COMPOSITE SCHEDULED: Errore durante la sincronizzazione delle cancellazioni"); + } + } + return totalProcessed; } catch (Exception ex) diff --git a/README.md b/README.md index 0e53693..82f3118 100644 --- a/README.md +++ b/README.md @@ -157,19 +157,53 @@ docker build -t data-coupler:local-windows -f Dockerfile.windows . ``` **Immagini Disponibili:** -- **Linux**: `ghcr.io/alessiodalsi/data-coupler:latest` +- **Linux/Multi-platform**: `ghcr.io/alessiodalsi/data-coupler:latest` - **Windows**: `ghcr.io/alessiodalsi/data-coupler:latest-windows` +- **Development**: `ghcr.io/alessiodalsi/data-coupler:development-latest` - **Dev**: `ghcr.io/alessiodalsi/data-coupler:dev-latest` - **Staging**: `ghcr.io/alessiodalsi/data-coupler:staging-latest` +**Note**: Il tag `latest` viene automaticamente aggiornato sia dal branch `main` che dal branch `development` per garantire che le ultime funzionalità siano sempre disponibili. Il tag `development-latest` è specifico per il branch development. + 📚 **Documentazione Docker Completa**: Vedi [DOCKER_DEPLOYMENT.md](DOCKER_DEPLOYMENT.md) e [GITHUB_ACTIONS_SETUP.md](GITHUB_ACTIONS_SETUP.md) +### 🔄 CI/CD Pipeline + +Il progetto supporta pipeline CI/CD automatiche su: + +**GitHub Actions** (`.github/workflows/docker-build.yml`): +- Build automatica su push ai branch `main`, `development`, `staging` +- Pubblicazione su GitHub Container Registry (`ghcr.io`) +- Multi-platform manifest (Linux + Windows) + +**Gitea Actions** (`.gitea/workflows/docker-build.yml`): +- Stessa configurazione di GitHub Actions +- Pubblicazione su Gitea Container Registry (`gitea.home-nas-ds.org`) +- Istanza Gitea self-hosted con registry abilitato +- Configurazione: `.gitea/workflows/README.md` + +**Immagini su Gitea** (self-hosted): +```bash +# Pull da Gitea Container Registry +docker pull gitea.home-nas-ds.org/alessio/data-coupler:latest + +# Versioni disponibili +docker pull gitea.home-nas-ds.org/alessio/data-coupler:development-latest +docker pull gitea.home-nas-ds.org/alessio/data-coupler:staging-latest +``` + +📚 **Setup Gitea**: Vedi [.gitea/workflows/README.md](.gitea/workflows/README.md) + ## Caratteristiche di Sicurezza - **Crittografia**: Le password vengono crittografate prima del salvataggio - **Validazione**: Validazione completa dei dati in input - **Isolamento**: Ogni progetto ha responsabilità specifiche - **Type Safety**: Uso di tipi forti per evitare errori +- **Deletion Sync Sicuro**: + - Disabilitato di default per prevenire eliminazioni accidentali + - Disponibile solo nelle schedulazioni con configurazione esplicita + - Warning chiaro nell'interfaccia utente per operazioni critiche ## Testing