feat: Implementazione completa sistema schedulazione con intervalli personalizzati

- Aggiunto supporto schedulazione con intervalli flessibili (secondi/minuti/ore/giorni/settimane/mesi)
- Esteso modello ProfileSchedule con campi IntervalValue e IntervalUnit
- Ottimizzato ScheduledJobService per controlli ogni 30s con esecuzione parallela
- Implementata interfaccia UI completa con anteprima real-time in italiano
- Aggiunta migrazione database AddIntervalSchedulingFields
- Implementati metodi calcolo NextExecutionTime per intervalli
- Aggiunta gestione tracking anti-duplicati e cleanup automatico
- Creata documentazione completa (6 file, 2500+ righe)

Modifiche tecniche:
- ProfileSchedule.cs: Nuovi campi e metodi CalculateNextInterval/GetScheduleDescription
- ScheduledJobService.cs: Ridotto check interval a 30s, aggiunto parallel processing
- ProfileScheduleService.cs: Supporto calcolo intervalli in UpdateNextExecutionTimeAsync
- Scheduling.razor: Aggiunta sezione UI per configurazione intervalli
- Scheduling.razor.cs: Implementato GetIntervalPreview() e gestione stato campi
This commit is contained in:
2025-10-02 01:12:39 +02:00
parent b76a6760fb
commit d042863a56
71 changed files with 17860 additions and 144 deletions
@@ -0,0 +1,145 @@
using CredentialManager.Services;
using Data_Coupler.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Data_Coupler.BackgroundServices;
/// <summary>
/// Servizio di background per l'esecuzione automatica delle schedulazioni
/// </summary>
public class ScheduleExecutorService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<ScheduleExecutorService> _logger;
private readonly TimeSpan _checkInterval = TimeSpan.FromMinutes(1); // Controlla ogni minuto
public ScheduleExecutorService(IServiceProvider serviceProvider, ILogger<ScheduleExecutorService> logger)
{
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("ScheduleExecutorService avviato. Controllo schedulazioni ogni {Interval} minuti.",
_checkInterval.TotalMinutes);
while (!stoppingToken.IsCancellationRequested)
{
try
{
await CheckAndExecuteSchedules();
await Task.Delay(_checkInterval, stoppingToken);
}
catch (OperationCanceledException)
{
_logger.LogInformation("ScheduleExecutorService arrestato.");
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel controllo schedulazioni");
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); // Attendi 5 minuti prima di riprovare
}
}
}
private async Task CheckAndExecuteSchedules()
{
using var scope = _serviceProvider.CreateScope();
var scheduleService = scope.ServiceProvider.GetRequiredService<IProfileScheduleService>();
var dataTransferService = scope.ServiceProvider.GetRequiredService<IDataTransferService>();
try
{
var pendingSchedules = await scheduleService.GetPendingExecutionsAsync();
if (pendingSchedules.Any())
{
_logger.LogInformation("Trovate {Count} schedulazioni in attesa di esecuzione", pendingSchedules.Count);
}
foreach (var schedule in pendingSchedules)
{
try
{
await ExecuteSchedule(schedule, scheduleService, dataTransferService);
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nell'esecuzione della schedulazione {ScheduleName} (ID: {ScheduleId})",
schedule.Name, schedule.Id);
// Aggiorna lo status dell'esecuzione fallita
await scheduleService.UpdateExecutionStatusAsync(schedule.Id, "failed",
$"Errore nell'esecuzione: {ex.Message}");
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel recupero delle schedulazioni in attesa");
}
}
private async Task ExecuteSchedule(CredentialManager.Models.ProfileSchedule schedule,
IProfileScheduleService scheduleService,
IDataTransferService dataTransferService)
{
_logger.LogInformation("Esecuzione schedulazione {ScheduleName} (ID: {ScheduleId}) iniziata",
schedule.Name, schedule.Id);
// Aggiorna lo status a "running"
await scheduleService.UpdateExecutionStatusAsync(schedule.Id, "running",
"Esecuzione automatica avviata");
try
{
if (schedule.Profile == null)
{
throw new InvalidOperationException("Profilo associato alla schedulazione non trovato");
}
// Esegui il trasferimento dati
var result = await dataTransferService.ExecuteProfileAsync(schedule.Profile);
// Aggiorna lo status con il risultato
var status = result.IsSuccess ? "success" : "failed";
var message = result.IsSuccess
? $"Esecuzione completata con successo in {result.Duration.TotalSeconds:F1} secondi. " +
$"{result.RecordsProcessed} record elaborati."
: $"Esecuzione fallita: {result.ErrorMessage}";
await scheduleService.UpdateExecutionStatusAsync(schedule.Id, status, message, result.RecordsProcessed);
if (result.IsSuccess)
{
_logger.LogInformation("Schedulazione {ScheduleName} completata con successo. " +
"Record processati: {RecordsProcessed}, Durata: {Duration}s",
schedule.Name, result.RecordsProcessed, result.Duration.TotalSeconds);
}
else
{
_logger.LogWarning("Schedulazione {ScheduleName} fallita: {ErrorMessage}",
schedule.Name, result.ErrorMessage);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore durante l'esecuzione della schedulazione {ScheduleName}", schedule.Name);
await scheduleService.UpdateExecutionStatusAsync(schedule.Id, "failed",
$"Errore nell'esecuzione: {ex.Message}");
throw; // Re-throw per permettere la gestione a livello superiore
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("ScheduleExecutorService in fase di arresto...");
await base.StopAsync(stoppingToken);
}
}
@@ -0,0 +1,327 @@
using CredentialManager.Services;
using CredentialManager.Models;
using Data_Coupler.Services;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
namespace Data_Coupler.BackgroundServices;
/// <summary>
/// Background service per l'esecuzione automatica delle schedulazioni
/// </summary>
public class ScheduledJobService : BackgroundService
{
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ILogger<ScheduledJobService> _logger;
private TimeSpan _checkInterval = TimeSpan.FromSeconds(30); // Controlla ogni 30 secondi per supportare intervalli brevi
private readonly Dictionary<int, DateTime> _runningSchedules = new(); // Tiene traccia delle schedulazioni in esecuzione
public ScheduledJobService(
IServiceScopeFactory serviceScopeFactory,
ILogger<ScheduledJobService> logger)
{
_serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("ScheduledJobService avviato");
// Attendi alcuni secondi prima di iniziare per permettere la completa inizializzazione dell'app
try
{
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
}
catch (OperationCanceledException)
{
_logger.LogInformation("ScheduledJobService cancellato durante l'inizializzazione");
return;
}
while (!stoppingToken.IsCancellationRequested)
{
try
{
await CheckAndExecutePendingSchedules(stoppingToken);
}
catch (OperationCanceledException)
{
_logger.LogInformation("ScheduledJobService cancellato");
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore durante il controllo delle schedulazioni");
// In caso di errore grave, attendi di più prima del prossimo tentativo
try
{
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
catch (OperationCanceledException)
{
break;
}
continue;
}
try
{
await Task.Delay(_checkInterval, stoppingToken);
}
catch (OperationCanceledException)
{
break;
}
}
_logger.LogInformation("ScheduledJobService arrestato");
}
private async Task CheckAndExecutePendingSchedules(CancellationToken cancellationToken)
{
using var scope = _serviceScopeFactory.CreateScope();
var scheduleService = scope.ServiceProvider.GetRequiredService<IProfileScheduleService>();
var dataTransferService = scope.ServiceProvider.GetRequiredService<IDataTransferService>();
try
{
// Ottieni le schedulazioni che devono essere eseguite
var pendingSchedules = await scheduleService.GetPendingExecutionsAsync();
if (!pendingSchedules.Any())
{
_logger.LogTrace("Nessuna schedulazione in sospeso trovata");
return;
}
_logger.LogInformation("Trovate {Count} schedulazioni da eseguire", pendingSchedules.Count);
// Pulisci le schedulazioni completate dal tracking
CleanupRunningSchedules();
foreach (var schedule in pendingSchedules)
{
if (cancellationToken.IsCancellationRequested)
break;
// Verifica se la schedulazione è già in esecuzione
if (IsScheduleRunning(schedule.Id))
{
_logger.LogDebug("Schedulazione {ScheduleId} già in esecuzione, salto", schedule.Id);
continue;
}
// Esegui la schedulazione in modo asincrono senza attendere
// Questo permette di eseguire più schedulazioni in parallelo
_ = Task.Run(async () =>
{
try
{
await ExecuteScheduleAsync(schedule, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nell'esecuzione asincrona della schedulazione {ScheduleId}", schedule.Id);
}
}, cancellationToken);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore durante il controllo delle schedulazioni pendenti");
}
}
private bool IsScheduleRunning(int scheduleId)
{
lock (_runningSchedules)
{
return _runningSchedules.ContainsKey(scheduleId);
}
}
private void MarkScheduleAsRunning(int scheduleId)
{
lock (_runningSchedules)
{
_runningSchedules[scheduleId] = DateTime.Now;
}
}
private void MarkScheduleAsCompleted(int scheduleId)
{
lock (_runningSchedules)
{
_runningSchedules.Remove(scheduleId);
}
}
private void CleanupRunningSchedules()
{
lock (_runningSchedules)
{
var timeout = DateTime.Now.AddHours(-1); // Se una schedulazione è "running" da più di 1 ora, considerala bloccata
var staleSchedules = _runningSchedules.Where(x => x.Value < timeout).Select(x => x.Key).ToList();
foreach (var scheduleId in staleSchedules)
{
_logger.LogWarning("Rimozione schedulazione {ScheduleId} da tracking (timeout esecuzione)", scheduleId);
_runningSchedules.Remove(scheduleId);
}
}
}
private async Task ExecuteScheduleAsync(
ProfileSchedule schedule,
CancellationToken cancellationToken)
{
ScheduleExecutionHistory? executionHistory = null;
// Marca la schedulazione come in esecuzione
MarkScheduleAsRunning(schedule.Id);
try
{
// Crea un nuovo scope per questa esecuzione
using var scope = _serviceScopeFactory.CreateScope();
var scheduleService = scope.ServiceProvider.GetRequiredService<IProfileScheduleService>();
var dataTransferService = scope.ServiceProvider.GetRequiredService<IDataTransferService>();
_logger.LogInformation("Esecuzione automatica schedulazione {ScheduleId} - {ScheduleName}",
schedule.Id, schedule.Name);
// Controlla se la schedulazione è ancora valida per l'esecuzione
if (!IsScheduleReadyForExecution(schedule))
{
_logger.LogDebug("Schedulazione {ScheduleId} non più pronta per l'esecuzione", schedule.Id);
return;
}
// Crea record nello storico
executionHistory = new ScheduleExecutionHistory
{
ScheduleId = schedule.Id,
ProfileId = schedule.ProfileId,
ProfileName = schedule.Profile?.Name ?? "Unknown",
StartTime = DateTime.Now,
Status = "running",
TriggerType = "automatic",
TriggeredBy = "System",
SourceType = schedule.Profile?.SourceType,
DestinationType = schedule.Profile?.DestinationType,
SourceInfo = schedule.SourceDatabaseOverride != null ? $"Database Override: {schedule.SourceDatabaseOverride}" : null,
DestinationInfo = schedule.DestinationDatabaseOverride != null ? $"Database Override: {schedule.DestinationDatabaseOverride}" : null,
Message = "Esecuzione automatica avviata"
};
executionHistory = await scheduleService.CreateExecutionHistoryAsync(executionHistory);
// Aggiorna lo status della schedulazione
await scheduleService.UpdateExecutionStatusAsync(schedule.Id, "running",
"Esecuzione automatica avviata");
// Esegui il trasferimento dati
if (schedule.Profile == null)
{
throw new InvalidOperationException($"Profilo non trovato per la schedulazione {schedule.Id}");
}
var result = await dataTransferService.ExecuteProfileAsync(
schedule.Profile,
schedule.SourceDatabaseOverride,
schedule.DestinationDatabaseOverride);
// Aggiorna lo storico con il risultato
executionHistory.EndTime = DateTime.Now;
executionHistory.Status = result.IsSuccess ? "success" : "failed";
executionHistory.RecordsProcessed = result.RecordsProcessed;
executionHistory.Message = result.IsSuccess
? $"Esecuzione automatica completata con successo. {result.RecordsProcessed} record elaborati in {result.Duration.TotalSeconds:F2} secondi."
: $"Esecuzione automatica fallita: {result.ErrorMessage}";
if (!result.IsSuccess)
{
executionHistory.ErrorDetails = string.Join(Environment.NewLine, result.ErrorDetails);
}
// Aggiungi informazioni aggiuntive se disponibili
if (result.AdditionalInfo.Any())
{
executionHistory.AdditionalInfo = System.Text.Json.JsonSerializer.Serialize(result.AdditionalInfo);
}
await scheduleService.UpdateExecutionHistoryAsync(executionHistory);
// Aggiorna lo status della schedulazione
var status = result.IsSuccess ? "success" : "failed";
var message = result.IsSuccess
? $"Esecuzione automatica completata con successo. {result.RecordsProcessed} record elaborati."
: $"Esecuzione automatica fallita: {result.ErrorMessage}";
await scheduleService.UpdateExecutionStatusAsync(schedule.Id, status, message, result.RecordsProcessed);
// Aggiorna la prossima data di esecuzione
await scheduleService.UpdateNextExecutionTimeAsync(schedule.Id);
_logger.LogInformation("Schedulazione {ScheduleId} eseguita con successo: {RecordsProcessed} record, durata {Duration}s",
schedule.Id, result.RecordsProcessed, result.Duration.TotalSeconds);
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore durante l'esecuzione automatica della schedulazione {ScheduleId}", schedule.Id);
// Crea un nuovo scope per gestire l'errore
try
{
using var errorScope = _serviceScopeFactory.CreateScope();
var scheduleService = errorScope.ServiceProvider.GetRequiredService<IProfileScheduleService>();
// Aggiorna lo storico in caso di eccezione
if (executionHistory != null)
{
executionHistory.EndTime = DateTime.Now;
executionHistory.Status = "failed";
executionHistory.Message = $"Errore durante l'esecuzione automatica: {ex.Message}";
executionHistory.ErrorDetails = ex.ToString();
await scheduleService.UpdateExecutionHistoryAsync(executionHistory);
}
await scheduleService.UpdateExecutionStatusAsync(schedule.Id, "failed",
$"Errore durante l'esecuzione automatica: {ex.Message}");
}
catch (Exception innerEx)
{
_logger.LogError(innerEx, "Errore durante l'aggiornamento dello stato di errore per la schedulazione {ScheduleId}", schedule.Id);
}
}
finally
{
// Rimuovi la schedulazione dal tracking
MarkScheduleAsCompleted(schedule.Id);
}
}
private bool IsScheduleReadyForExecution(ProfileSchedule schedule)
{
// Verifica che la schedulazione sia attiva e abilitata
if (!schedule.IsActive || !schedule.IsEnabled)
return false;
// Verifica che ci sia una prossima esecuzione programmata
if (!schedule.NextExecutionTime.HasValue)
return false;
// Per schedulazioni a intervalli, usa una tolleranza più stretta
var tolerance = schedule.ScheduleType == "interval"
? TimeSpan.FromSeconds(30) // 30 secondi per intervalli
: TimeSpan.FromMinutes(1); // 1 minuto per altre schedulazioni
var now = DateTime.Now;
var nextExecution = schedule.NextExecutionTime.Value;
return nextExecution <= now.Add(tolerance);
}
}
+558
View File
@@ -0,0 +1,558 @@
@using Data_Coupler.Services
@using Data_Coupler.Models
@inject IBackupService BackupService
@inject IJSRuntime JSRuntime
@inject ILogger<BackupTab> Logger
<div class="settings-section">
<h4>
<i class="fas fa-download text-primary me-2"></i>
Backup e Ripristino Dati
</h4>
<p class="text-muted">
Gestisci il backup e il ripristino di profili, credenziali, associazioni e schedule.
I backup non includono password o API keys per motivi di sicurezza.
</p>
<div class="row">
<!-- Export Section -->
<div class="col-md-6">
<div class="card h-100">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-upload text-success me-2"></i>
Esporta Backup
</h5>
</div>
<div class="card-body">
<p class="card-text">
Crea un backup completo del sistema con tutti i dati configurati.
</p>
<!-- Opzioni Export -->
<div class="mb-3">
<label class="form-label fw-bold">Componenti da includere:</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="exportOptions.IncludeProfiles" id="exportProfiles">
<label class="form-check-label" for="exportProfiles">
<i class="fas fa-user-cog me-1"></i> Profili Data Coupler
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="exportOptions.IncludeCredentials" id="exportCredentials">
<label class="form-check-label" for="exportCredentials">
<i class="fas fa-key me-1"></i> Credenziali (senza password)
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="exportOptions.IncludeKeyAssociations" id="exportAssociations">
<label class="form-check-label" for="exportAssociations">
<i class="fas fa-link me-1"></i> Associazioni Chiavi
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="exportOptions.IncludeProfileSchedules" id="exportSchedules">
<label class="form-check-label" for="exportSchedules">
<i class="fas fa-clock me-1"></i> Schedule Profili
</label>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="exportOptions.IncludeOnlyActiveRecords" id="exportOnlyActive">
<label class="form-check-label" for="exportOnlyActive">
Solo record attivi
</label>
</div>
</div>
<div class="mb-3">
<label for="exportDescription" class="form-label">Descrizione (opzionale):</label>
<input type="text" class="form-control" id="exportDescription" @bind="exportOptions.Description"
placeholder="Backup settimanale, Configurazione produzione, etc.">
</div>
<div class="mb-3">
<label for="exportCreatedBy" class="form-label">Creato da:</label>
<input type="text" class="form-control" id="exportCreatedBy" @bind="exportOptions.CreatedBy"
placeholder="Nome utente o sistema">
</div>
<button class="btn btn-success" @onclick="ExportBackup" disabled="@isExporting">
@if (isExporting)
{
<div class="spinner-border spinner-border-sm me-2" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<text>Esportazione...</text>
}
else
{
<i class="fas fa-download me-2"></i>
<text>Crea Backup</text>
}
</button>
@if (lastExportResult != null)
{
<div class="mt-3">
<div class="alert alert-@(lastExportResult.Success ? "success" : "danger") alert-sm">
<div class="d-flex justify-content-between align-items-start">
<div>
<strong>@(lastExportResult.Success ? "Successo" : "Errore"):</strong>
<div>@lastExportResult.Message</div>
@if (lastExportResult.Success)
{
<small class="text-muted">
Durata: @lastExportResult.Duration.TotalSeconds.ToString("F1")s |
Record: @(lastExportResult.ProcessedCounts.Profiles + lastExportResult.ProcessedCounts.Credentials + lastExportResult.ProcessedCounts.KeyAssociations + lastExportResult.ProcessedCounts.ProfileSchedules)
</small>
}
</div>
<button type="button" class="btn-close btn-close-sm" @onclick="() => lastExportResult = null"></button>
</div>
</div>
</div>
}
</div>
</div>
</div>
<!-- Import Section -->
<div class="col-md-6">
<div class="card h-100">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-download text-primary me-2"></i>
Importa Backup
</h5>
</div>
<div class="card-body">
<p class="card-text">
Ripristina i dati da un file di backup precedentemente creato.
</p>
<!-- File Upload -->
<div class="mb-3">
<label for="backupFile" class="form-label fw-bold">Seleziona file backup:</label>
<InputFile class="form-control" id="backupFile"
accept=".json" OnChange="OnFileSelected" />
<div class="form-text">
Seleziona un file .json di backup di Data Coupler
</div>
</div>
@if (selectedBackupInfo != null)
{
<div class="mb-3">
<div class="card border-info">
<div class="card-header bg-info text-white">
<h6 class="mb-0">
<i class="fas fa-info-circle me-2"></i>
Informazioni Backup
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-6">
<strong>Versione:</strong> @selectedBackupInfo.Metadata.Version<br>
<strong>Creato:</strong> @selectedBackupInfo.Metadata.CreatedAt.ToString("dd/MM/yyyy HH:mm")<br>
@if (!string.IsNullOrEmpty(selectedBackupInfo.Metadata.CreatedBy))
{
<strong>Da:</strong> @selectedBackupInfo.Metadata.CreatedBy<br>
}
</div>
<div class="col-6">
<strong>Profili:</strong> @selectedBackupInfo.Profiles.Count<br>
<strong>Credenziali:</strong> @selectedBackupInfo.Credentials.Count<br>
<strong>Associazioni:</strong> @selectedBackupInfo.KeyAssociations.Count<br>
<strong>Schedule:</strong> @selectedBackupInfo.ProfileSchedules.Count<br>
</div>
@if (!string.IsNullOrEmpty(selectedBackupInfo.Metadata.Description))
{
<div class="col-12 mt-2">
<strong>Descrizione:</strong> @selectedBackupInfo.Metadata.Description
</div>
}
</div>
</div>
</div>
</div>
<!-- Opzioni Import -->
<div class="mb-3">
<label class="form-label fw-bold">Componenti da ripristinare:</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="restoreOptions.RestoreProfiles" id="restoreProfiles">
<label class="form-check-label" for="restoreProfiles">
<i class="fas fa-user-cog me-1"></i> Profili (@selectedBackupInfo.Profiles.Count)
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="restoreOptions.RestoreCredentials" id="restoreCredentials">
<label class="form-check-label" for="restoreCredentials">
<i class="fas fa-key me-1"></i> Credenziali (@selectedBackupInfo.Credentials.Count)
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="restoreOptions.RestoreKeyAssociations" id="restoreAssociations">
<label class="form-check-label" for="restoreAssociations">
<i class="fas fa-link me-1"></i> Associazioni (@selectedBackupInfo.KeyAssociations.Count)
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="restoreOptions.RestoreProfileSchedules" id="restoreSchedules">
<label class="form-check-label" for="restoreSchedules">
<i class="fas fa-clock me-1"></i> Schedule (@selectedBackupInfo.ProfileSchedules.Count)
</label>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="restoreOptions.OverwriteExisting" id="overwriteExisting">
<label class="form-check-label" for="overwriteExisting">
<i class="fas fa-exclamation-triangle text-warning me-1"></i>
Sovrascrivi dati esistenti
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="restoreOptions.CreateBackupBeforeRestore" id="createBackupBefore">
<label class="form-check-label" for="createBackupBefore">
<i class="fas fa-shield-alt text-info me-1"></i>
Crea backup di sicurezza prima del ripristino
</label>
</div>
</div>
<div class="mb-3">
<label for="importedBy" class="form-label">Importato da:</label>
<input type="text" class="form-control" id="importedBy" @bind="restoreOptions.ImportedBy"
placeholder="Nome utente">
</div>
}
<button class="btn btn-primary" @onclick="ImportBackup"
disabled="@(isImporting || selectedBackupInfo == null)">
@if (isImporting)
{
<div class="spinner-border spinner-border-sm me-2" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<text>Importazione...</text>
}
else
{
<i class="fas fa-upload me-2"></i>
<text>Importa Backup</text>
}
</button>
@if (lastImportResult != null)
{
<div class="mt-3">
<div class="alert alert-@(lastImportResult.Success ? "success" : "danger") alert-sm">
<div class="d-flex justify-content-between align-items-start">
<div>
<strong>@(lastImportResult.Success ? "Successo" : "Errore"):</strong>
<div>@lastImportResult.Message</div>
@if (lastImportResult.Success)
{
<small class="text-muted">
Durata: @lastImportResult.Duration.TotalSeconds.ToString("F1")s |
Record importati: @(lastImportResult.ProcessedCounts.Profiles + lastImportResult.ProcessedCounts.Credentials + lastImportResult.ProcessedCounts.KeyAssociations + lastImportResult.ProcessedCounts.ProfileSchedules)
</small>
}
@if (lastImportResult.Warnings.Any())
{
<div class="mt-2">
<strong>Avvisi:</strong>
<ul class="mb-0">
@foreach (var warning in lastImportResult.Warnings)
{
<li><small>@warning</small></li>
}
</ul>
</div>
}
</div>
<button type="button" class="btn-close btn-close-sm" @onclick="() => lastImportResult = null"></button>
</div>
</div>
</div>
}
</div>
</div>
</div>
</div>
<!-- Backup History Section -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-history text-secondary me-2"></i>
Backup Recenti
</h5>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<p class="text-muted mb-0">
Elenco dei file di backup nella cartella Documenti/DataCoupler/Backups
</p>
<button class="btn btn-outline-secondary btn-sm" @onclick="RefreshBackupList">
<i class="fas fa-refresh me-1"></i>
Aggiorna
</button>
</div>
@if (recentBackups.Any())
{
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead class="table-light">
<tr>
<th>Nome File</th>
<th>Data Creazione</th>
<th>Dimensione</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
@foreach (var backup in recentBackups.Take(10))
{
<tr>
<td>
<i class="fas fa-file-code text-primary me-2"></i>
@backup.Name
</td>
<td>
<small>@backup.CreationTime.ToString("dd/MM/yyyy HH:mm")</small>
</td>
<td>
<small>@FormatFileSize(backup.Length)</small>
</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary"
@onclick="() => DownloadBackup(backup.FullName)"
title="Scarica">
<i class="fas fa-download"></i>
</button>
<button class="btn btn-outline-info"
@onclick='() => OnShowToast.InvokeAsync(("Funzione non disponibile", "info"))'
title="Carica per import">
<i class="fas fa-upload"></i>
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
else
{
<div class="text-center py-4">
<i class="fas fa-folder-open text-muted fa-3x mb-3"></i>
<p class="text-muted">Nessun backup trovato</p>
</div>
}
</div>
</div>
</div>
</div>
</div>
@code {
[Parameter] public EventCallback<(string message, string type)> OnShowToast { get; set; }
private BackupOptions exportOptions = new()
{
IncludeProfiles = true,
IncludeCredentials = true,
IncludeKeyAssociations = true,
IncludeProfileSchedules = true,
IncludeOnlyActiveRecords = true
};
private RestoreOptions restoreOptions = new()
{
RestoreProfiles = true,
RestoreCredentials = false, // Default false per sicurezza
RestoreKeyAssociations = true,
RestoreProfileSchedules = true,
OverwriteExisting = false,
CreateBackupBeforeRestore = true
};
private bool isExporting = false;
private bool isImporting = false;
private BackupOperationResult? lastExportResult;
private BackupOperationResult? lastImportResult;
private SystemBackupData? selectedBackupInfo;
private List<FileInfo> recentBackups = new();
protected override async Task OnInitializedAsync()
{
await RefreshBackupList();
}
private async Task ExportBackup()
{
try
{
isExporting = true;
lastExportResult = null;
StateHasChanged();
lastExportResult = await BackupService.ExportBackupAsync(exportOptions);
if (lastExportResult.Success)
{
await OnShowToast.InvokeAsync(($"Backup creato con successo", "success"));
await RefreshBackupList();
}
else
{
await OnShowToast.InvokeAsync(("Errore durante la creazione del backup", "error"));
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore esportazione backup");
await OnShowToast.InvokeAsync(($"Errore: {ex.Message}", "error"));
}
finally
{
isExporting = false;
StateHasChanged();
}
}
private async Task OnFileSelected(InputFileChangeEventArgs e)
{
try
{
var file = e.File;
if (file != null)
{
using var stream = file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024); // 10MB max
using var reader = new StreamReader(stream);
var content = await reader.ReadToEndAsync();
selectedBackupInfo = await BackupService.GetBackupInfoAsync(content);
StateHasChanged();
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore lettura file backup");
await OnShowToast.InvokeAsync(($"Errore lettura file: {ex.Message}", "error"));
}
}
private async Task ImportBackup()
{
try
{
isImporting = true;
lastImportResult = null;
StateHasChanged();
if (selectedBackupInfo != null)
{
// Serializza il backup selezionato per l'import
var backupContent = System.Text.Json.JsonSerializer.Serialize(selectedBackupInfo);
lastImportResult = await BackupService.ImportBackupFromJsonAsync(backupContent, restoreOptions);
if (lastImportResult.Success)
{
await OnShowToast.InvokeAsync(("Backup importato con successo", "success"));
}
else
{
await OnShowToast.InvokeAsync(("Errore durante l'importazione", "error"));
}
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore importazione backup");
await OnShowToast.InvokeAsync(($"Errore: {ex.Message}", "error"));
}
finally
{
isImporting = false;
StateHasChanged();
}
}
private Task RefreshBackupList()
{
try
{
var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
var backupFolder = Path.Combine(documentsPath, "DataCoupler", "Backups");
if (Directory.Exists(backupFolder))
{
var files = new DirectoryInfo(backupFolder)
.GetFiles("*.json")
.OrderByDescending(f => f.CreationTime)
.ToList();
recentBackups = files;
}
else
{
recentBackups = new List<FileInfo>();
}
StateHasChanged();
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Errore refresh lista backup");
}
return Task.CompletedTask;
}
private async Task DownloadBackup(string filePath)
{
try
{
var fileName = Path.GetFileName(filePath);
var bytes = await File.ReadAllBytesAsync(filePath);
var stream = new MemoryStream(bytes);
using var streamRef = new DotNetStreamReference(stream);
await JSRuntime.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef);
await OnShowToast.InvokeAsync(($"Download di {fileName} avviato", "info"));
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore download backup");
await OnShowToast.InvokeAsync(($"Errore download: {ex.Message}", "error"));
}
}
private string FormatFileSize(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB" };
double len = bytes;
int order = 0;
while (len >= 1024 && order < sizes.Length - 1)
{
order++;
len = len / 1024;
}
return $"{len:0.##} {sizes[order]}";
}
}
@@ -0,0 +1,573 @@
@inject ILogger<MaintenanceTab> Logger
<div class="settings-section">
<h4>
<i class="fas fa-tools text-success me-2"></i>
Manutenzione Sistema
</h4>
<p class="text-muted">
Strumenti per la manutenzione e l'ottimizzazione del sistema.
</p>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-database text-primary me-2"></i>
Database
</h5>
</div>
<div class="card-body">
<p class="card-text">Operazioni di manutenzione del database.</p>
<div class="mb-3">
<h6>Stato Database</h6>
<div class="d-flex justify-content-between align-items-center">
<span>Dimensione:</span>
<span class="badge bg-info">@databaseStats.SizeMB MB</span>
</div>
<div class="d-flex justify-content-between align-items-center mt-1">
<span>Ultima ottimizzazione:</span>
<span class="text-muted">@databaseStats.LastOptimization.ToString("dd/MM/yyyy HH:mm")</span>
</div>
</div>
<div class="d-grid gap-2">
<button class="btn btn-outline-primary" @onclick="OptimizeDatabase" disabled="@isOptimizing">
@if (isOptimizing)
{
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
}
else
{
<i class="fas fa-cogs me-1"></i>
}
Ottimizza Database
</button>
<button class="btn btn-outline-secondary" @onclick="AnalyzeDatabase" disabled="@isAnalyzing">
@if (isAnalyzing)
{
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
}
else
{
<i class="fas fa-search me-1"></i>
}
Analizza Integrità
</button>
<button class="btn btn-outline-warning" @onclick="CompactDatabase" disabled="@isCompacting">
@if (isCompacting)
{
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
}
else
{
<i class="fas fa-compress-alt me-1"></i>
}
Compatta Database
</button>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-trash-alt text-warning me-2"></i>
Pulizia Sistema
</h5>
</div>
<div class="card-body">
<p class="card-text">Strumenti per la pulizia dei dati temporanei.</p>
<div class="mb-3">
<h6>File temporanei</h6>
<div class="d-flex justify-content-between align-items-center">
<span>Dimensione cache:</span>
<span class="badge bg-secondary">@cleanupStats.CacheSizeMB MB</span>
</div>
<div class="d-flex justify-content-between align-items-center mt-1">
<span>File di log:</span>
<span class="badge bg-secondary">@cleanupStats.LogFileCount file</span>
</div>
</div>
<div class="d-grid gap-2">
<button class="btn btn-outline-warning" @onclick="ClearTempFiles" disabled="@isClearingTemp">
@if (isClearingTemp)
{
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
}
else
{
<i class="fas fa-broom me-1"></i>
}
Pulisci File Temporanei
</button>
<button class="btn btn-outline-secondary" @onclick="ClearOldLogs" disabled="@isClearingLogs">
@if (isClearingLogs)
{
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
}
else
{
<i class="fas fa-file-alt me-1"></i>
}
Pulisci Log Vecchi
</button>
<button class="btn btn-outline-info" @onclick="RefreshStats">
<i class="fas fa-sync-alt me-1"></i>
Aggiorna Statistiche
</button>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-chart-line text-info me-2"></i>
Monitoraggio Performance
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="text-center">
<h6>Memoria Utilizzata</h6>
<div class="progress mb-2">
<div class="progress-bar" role="progressbar" style="width: @(performanceStats.MemoryUsagePercent)%"></div>
</div>
<small class="text-muted">@performanceStats.MemoryUsageMB MB / @performanceStats.TotalMemoryMB MB</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h6>CPU</h6>
<div class="progress mb-2">
<div class="progress-bar bg-warning" role="progressbar" style="width: @(performanceStats.CpuUsagePercent)%"></div>
</div>
<small class="text-muted">@performanceStats.CpuUsagePercent%</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h6>Connessioni Attive</h6>
<h4 class="text-primary">@performanceStats.ActiveConnections</h4>
<small class="text-muted">di @performanceStats.MaxConnections max</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h6>Uptime</h6>
<h4 class="text-success">@performanceStats.UptimeHours h</h4>
<small class="text-muted">@performanceStats.StartTime.ToString("dd/MM HH:mm")</small>
</div>
</div>
</div>
<div class="mt-3">
<button class="btn btn-outline-primary" @onclick="GeneratePerformanceReport">
<i class="fas fa-file-chart-column me-1"></i>
Genera Report Performance
</button>
<button class="btn btn-outline-secondary ms-2" @onclick="RefreshPerformanceStats">
<i class="fas fa-sync-alt me-1"></i>
Aggiorna
</button>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-calendar-check text-primary me-2"></i>
Manutenzione Programmata
</h5>
</div>
<div class="card-body">
<p class="card-text">Configurazione delle attività automatiche.</p>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" @bind="maintenanceSettings.AutoOptimizeEnabled" id="autoOptimize">
<label class="form-check-label" for="autoOptimize">
Ottimizzazione automatica database
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" @bind="maintenanceSettings.AutoCleanupEnabled" id="autoCleanup">
<label class="form-check-label" for="autoCleanup">
Pulizia automatica file temporanei
</label>
</div>
<div class="mb-3">
<label for="maintenanceTime" class="form-label">Orario manutenzione:</label>
<input type="time" class="form-control" id="maintenanceTime" @bind="maintenanceSettings.ScheduledTime">
</div>
<div class="mb-3">
<label for="maintenanceFrequency" class="form-label">Frequenza:</label>
<select class="form-select" id="maintenanceFrequency" @bind="maintenanceSettings.Frequency">
<option value="Daily">Giornaliera</option>
<option value="Weekly">Settimanale</option>
<option value="Monthly">Mensile</option>
</select>
</div>
<button class="btn btn-primary" @onclick="SaveMaintenanceSettings">
<i class="fas fa-save me-1"></i>
Salva Configurazione
</button>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-history text-info me-2"></i>
Storico Attività
</h5>
</div>
<div class="card-body">
<p class="card-text">Ultime operazioni di manutenzione.</p>
<div class="maintenance-history">
@foreach (var activity in maintenanceHistory)
{
<div class="d-flex justify-content-between align-items-center mb-2">
<div>
<i class="@GetActivityIcon(activity.Type) me-2"></i>
@activity.Description
</div>
<small class="text-muted">@activity.Timestamp.ToString("dd/MM HH:mm")</small>
</div>
}
</div>
@if (!maintenanceHistory.Any())
{
<div class="text-muted text-center py-3">
<i class="fas fa-info-circle me-2"></i>
Nessuna attività di manutenzione recente
</div>
}
<button class="btn btn-outline-secondary btn-sm mt-2" @onclick="ClearMaintenanceHistory">
<i class="fas fa-trash-alt me-1"></i>
Pulisci Storico
</button>
</div>
</div>
</div>
</div>
</div>
@code {
[Parameter] public EventCallback<(string message, string type)> OnShowToast { get; set; }
private bool isOptimizing = false;
private bool isAnalyzing = false;
private bool isCompacting = false;
private bool isClearingTemp = false;
private bool isClearingLogs = false;
private DatabaseStats databaseStats = new()
{
SizeMB = 12.5,
LastOptimization = DateTime.Now.AddDays(-3)
};
private CleanupStats cleanupStats = new()
{
CacheSizeMB = 45.2,
LogFileCount = 127
};
private PerformanceStats performanceStats = new()
{
MemoryUsageMB = 234,
TotalMemoryMB = 512,
MemoryUsagePercent = 45,
CpuUsagePercent = 12,
ActiveConnections = 3,
MaxConnections = 100,
UptimeHours = 72,
StartTime = DateTime.Now.AddHours(-72)
};
private MaintenanceSettings maintenanceSettings = new()
{
AutoOptimizeEnabled = true,
AutoCleanupEnabled = true,
ScheduledTime = new TimeOnly(2, 0),
Frequency = "Weekly"
};
private List<MaintenanceActivity> maintenanceHistory = new()
{
new MaintenanceActivity { Type = "Optimization", Description = "Database ottimizzato", Timestamp = DateTime.Now.AddHours(-6) },
new MaintenanceActivity { Type = "Cleanup", Description = "File temporanei rimossi", Timestamp = DateTime.Now.AddDays(-1) },
new MaintenanceActivity { Type = "Backup", Description = "Backup automatico completato", Timestamp = DateTime.Now.AddDays(-2) }
};
protected override async Task OnInitializedAsync()
{
await RefreshStats();
}
private async Task OptimizeDatabase()
{
try
{
isOptimizing = true;
StateHasChanged();
await Task.Delay(3000); // Simula ottimizzazione
databaseStats.LastOptimization = DateTime.Now;
AddMaintenanceActivity("Optimization", "Database ottimizzato");
await OnShowToast.InvokeAsync(("Database ottimizzato con successo", "success"));
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore ottimizzazione database");
await OnShowToast.InvokeAsync(("Errore durante l'ottimizzazione", "error"));
}
finally
{
isOptimizing = false;
StateHasChanged();
}
}
private async Task AnalyzeDatabase()
{
try
{
isAnalyzing = true;
StateHasChanged();
await Task.Delay(2000);
await OnShowToast.InvokeAsync(("Analisi completata: nessun problema rilevato", "success"));
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore analisi database");
await OnShowToast.InvokeAsync(("Errore durante l'analisi", "error"));
}
finally
{
isAnalyzing = false;
StateHasChanged();
}
}
private async Task CompactDatabase()
{
try
{
isCompacting = true;
StateHasChanged();
await Task.Delay(4000);
databaseStats.SizeMB *= 0.8; // Simula riduzione dimensioni
AddMaintenanceActivity("Compact", "Database compattato");
await OnShowToast.InvokeAsync(("Database compattato con successo", "success"));
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore compattazione database");
await OnShowToast.InvokeAsync(("Errore durante la compattazione", "error"));
}
finally
{
isCompacting = false;
StateHasChanged();
}
}
private async Task ClearTempFiles()
{
try
{
isClearingTemp = true;
StateHasChanged();
await Task.Delay(1500);
cleanupStats.CacheSizeMB = 0;
AddMaintenanceActivity("Cleanup", "File temporanei rimossi");
await OnShowToast.InvokeAsync(("File temporanei rimossi", "success"));
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore pulizia file temporanei");
await OnShowToast.InvokeAsync(("Errore durante la pulizia", "error"));
}
finally
{
isClearingTemp = false;
StateHasChanged();
}
}
private async Task ClearOldLogs()
{
try
{
isClearingLogs = true;
StateHasChanged();
await Task.Delay(1000);
cleanupStats.LogFileCount = Math.Max(0, cleanupStats.LogFileCount - 50);
AddMaintenanceActivity("LogCleanup", "Log vecchi rimossi");
await OnShowToast.InvokeAsync(("Log vecchi rimossi", "success"));
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore pulizia log");
await OnShowToast.InvokeAsync(("Errore durante la pulizia log", "error"));
}
finally
{
isClearingLogs = false;
StateHasChanged();
}
}
private async Task RefreshStats()
{
await Task.Delay(300);
// Simula aggiornamento statistiche
cleanupStats.CacheSizeMB = Random.Shared.Next(20, 80);
cleanupStats.LogFileCount = Random.Shared.Next(50, 200);
StateHasChanged();
}
private async Task RefreshPerformanceStats()
{
await Task.Delay(500);
// Simula aggiornamento performance
performanceStats.MemoryUsagePercent = Random.Shared.Next(30, 70);
performanceStats.CpuUsagePercent = Random.Shared.Next(5, 25);
performanceStats.ActiveConnections = Random.Shared.Next(1, 10);
StateHasChanged();
}
private async Task GeneratePerformanceReport()
{
await OnShowToast.InvokeAsync(("Report performance generato", "info"));
}
private async Task SaveMaintenanceSettings()
{
try
{
await Task.Delay(300);
await OnShowToast.InvokeAsync(("Configurazione manutenzione salvata", "success"));
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore salvataggio configurazione");
await OnShowToast.InvokeAsync(("Errore salvataggio configurazione", "error"));
}
}
private async Task ClearMaintenanceHistory()
{
maintenanceHistory.Clear();
await OnShowToast.InvokeAsync(("Storico attività pulito", "info"));
StateHasChanged();
}
private void AddMaintenanceActivity(string type, string description)
{
maintenanceHistory.Insert(0, new MaintenanceActivity
{
Type = type,
Description = description,
Timestamp = DateTime.Now
});
// Mantieni solo le ultime 10 attività
if (maintenanceHistory.Count > 10)
{
maintenanceHistory.RemoveAt(maintenanceHistory.Count - 1);
}
}
private string GetActivityIcon(string type) => type switch
{
"Optimization" => "fas fa-cogs text-primary",
"Cleanup" => "fas fa-broom text-warning",
"Backup" => "fas fa-download text-success",
"Compact" => "fas fa-compress-alt text-info",
"LogCleanup" => "fas fa-file-alt text-secondary",
_ => "fas fa-cog text-muted"
};
private class DatabaseStats
{
public double SizeMB { get; set; }
public DateTime LastOptimization { get; set; }
}
private class CleanupStats
{
public double CacheSizeMB { get; set; }
public int LogFileCount { get; set; }
}
private class PerformanceStats
{
public int MemoryUsageMB { get; set; }
public int TotalMemoryMB { get; set; }
public int MemoryUsagePercent { get; set; }
public int CpuUsagePercent { get; set; }
public int ActiveConnections { get; set; }
public int MaxConnections { get; set; }
public int UptimeHours { get; set; }
public DateTime StartTime { get; set; }
}
private class MaintenanceSettings
{
public bool AutoOptimizeEnabled { get; set; }
public bool AutoCleanupEnabled { get; set; }
public TimeOnly ScheduledTime { get; set; }
public string Frequency { get; set; } = "Weekly";
}
private class MaintenanceActivity
{
public string Type { get; set; } = "";
public string Description { get; set; } = "";
public DateTime Timestamp { get; set; }
}
}
+346
View File
@@ -0,0 +1,346 @@
@inject ILogger<SecurityTab> Logger
<div class="settings-section">
<h4>
<i class="fas fa-shield-alt text-primary me-2"></i>
Sicurezza e Privacy
</h4>
<p class="text-muted">
Configurazioni per la sicurezza dei dati e la gestione delle credenziali.
</p>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-lock text-danger me-2"></i>
Crittografia
</h5>
</div>
<div class="card-body">
<p class="card-text">Gestione della crittografia delle credenziali.</p>
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
<strong>Data Protection API Attiva</strong><br>
Le credenziali sono crittografate utilizzando la Data Protection API di .NET.
</div>
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center">
<span>Stato Crittografia:</span>
<span class="badge bg-success">
<i class="fas fa-check-circle me-1"></i>
Attiva
</span>
</div>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center">
<span>Algoritmo:</span>
<span class="badge bg-info">AES-256</span>
</div>
</div>
<button class="btn btn-outline-warning btn-sm" @onclick="RegenerateKeys" disabled="@isRegeneratingKeys">
@if (isRegeneratingKeys)
{
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
}
else
{
<i class="fas fa-key me-1"></i>
}
Rigenera Chiavi
</button>
<div class="form-text">
<i class="fas fa-exclamation-triangle text-warning me-1"></i>
Attenzione: La rigenerazione delle chiavi renderà inaccessibili le credenziali esistenti
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-user-shield text-success me-2"></i>
Audit e Logging
</h5>
</div>
<div class="card-body">
<p class="card-text">Configurazione del logging di sicurezza.</p>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" @bind="securitySettings.LogCredentialAccess" id="logCredentialAccess">
<label class="form-check-label" for="logCredentialAccess">
Log accesso alle credenziali
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" @bind="securitySettings.LogDataTransfers" id="logDataTransfers">
<label class="form-check-label" for="logDataTransfers">
Log trasferimenti dati
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" @bind="securitySettings.LogFailedOperations" id="logFailedOperations">
<label class="form-check-label" for="logFailedOperations">
Log operazioni fallite
</label>
</div>
<div class="mb-3">
<label for="logRetentionDays" class="form-label">Giorni di ritenzione log:</label>
<input type="number" class="form-control" id="logRetentionDays" @bind="securitySettings.LogRetentionDays"
min="1" max="365" step="1">
</div>
<button class="btn btn-primary btn-sm" @onclick="SaveSecuritySettings">
<i class="fas fa-save me-1"></i>
Salva Impostazioni
</button>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-network-wired text-primary me-2"></i>
Connessioni Sicure
</h5>
</div>
<div class="card-body">
<p class="card-text">Impostazioni per le connessioni di rete.</p>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" @bind="securitySettings.EnforceHttps" id="enforceHttps">
<label class="form-check-label" for="enforceHttps">
Forza HTTPS per API REST
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" @bind="securitySettings.ValidateSslCertificates" id="validateSsl">
<label class="form-check-label" for="validateSsl">
Valida certificati SSL/TLS
</label>
</div>
<div class="mb-3">
<label for="connectionTimeout" class="form-label">Timeout connessioni (secondi):</label>
<input type="number" class="form-control" id="connectionTimeout" @bind="securitySettings.ConnectionTimeoutSeconds"
min="5" max="300" step="5">
</div>
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>Nota:</strong> Disabilitare la validazione SSL solo in ambienti di sviluppo sicuri.
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-database text-warning me-2"></i>
Backup Sicurezza
</h5>
</div>
<div class="card-body">
<p class="card-text">Configurazione dei backup di sicurezza.</p>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" @bind="securitySettings.AutoBackupEnabled" id="autoBackup">
<label class="form-check-label" for="autoBackup">
Backup automatico giornaliero
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" @bind="securitySettings.EncryptBackups" id="encryptBackups">
<label class="form-check-label" for="encryptBackups">
Cripta file di backup
</label>
</div>
<div class="mb-3">
<label for="backupRetentionDays" class="form-label">Giorni di ritenzione backup:</label>
<input type="number" class="form-control" id="backupRetentionDays" @bind="securitySettings.BackupRetentionDays"
min="1" max="365" step="1">
</div>
@if (securitySettings.AutoBackupEnabled)
{
<div class="alert alert-info">
<i class="fas fa-clock me-2"></i>
Prossimo backup automatico: <strong>Domani alle 02:00</strong>
</div>
}
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-exclamation-triangle text-danger me-2"></i>
Azioni Critiche
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Pulizia Cache Credenziali</h6>
<p class="text-muted">Pulisce la cache delle credenziali in memoria.</p>
<button class="btn btn-outline-warning" @onclick="ClearCredentialCache">
<i class="fas fa-broom me-1"></i>
Pulisci Cache
</button>
</div>
<div class="col-md-6">
<h6>Reset Configurazione Sicurezza</h6>
<p class="text-muted">Ripristina le impostazioni di sicurezza ai valori predefiniti.</p>
<button class="btn btn-outline-danger" @onclick="ResetSecuritySettings">
<i class="fas fa-undo me-1"></i>
Reset Impostazioni
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@code {
[Parameter] public EventCallback<(string message, string type)> OnShowToast { get; set; }
private bool isRegeneratingKeys = false;
private SecuritySettings securitySettings = new()
{
LogCredentialAccess = true,
LogDataTransfers = true,
LogFailedOperations = true,
LogRetentionDays = 30,
EnforceHttps = true,
ValidateSslCertificates = true,
ConnectionTimeoutSeconds = 30,
AutoBackupEnabled = false,
EncryptBackups = true,
BackupRetentionDays = 30
};
private async Task SaveSecuritySettings()
{
try
{
// Implementazione semplificata - in produzione salvare nel database o file di configurazione
await Task.Delay(500);
await OnShowToast.InvokeAsync(("Impostazioni di sicurezza salvate", "success"));
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore salvataggio impostazioni sicurezza");
await OnShowToast.InvokeAsync(("Errore salvataggio impostazioni", "error"));
}
}
private async Task RegenerateKeys()
{
try
{
isRegeneratingKeys = true;
StateHasChanged();
// Simula rigenerazione chiavi
await Task.Delay(2000);
await OnShowToast.InvokeAsync(("Chiavi di crittografia rigenerate", "success"));
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore rigenerazione chiavi");
await OnShowToast.InvokeAsync(("Errore rigenerazione chiavi", "error"));
}
finally
{
isRegeneratingKeys = false;
StateHasChanged();
}
}
private async Task ClearCredentialCache()
{
try
{
// Implementazione semplificata
await Task.Delay(500);
await OnShowToast.InvokeAsync(("Cache credenziali pulita", "success"));
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore pulizia cache");
await OnShowToast.InvokeAsync(("Errore pulizia cache", "error"));
}
}
private async Task ResetSecuritySettings()
{
try
{
securitySettings = new SecuritySettings
{
LogCredentialAccess = true,
LogDataTransfers = true,
LogFailedOperations = true,
LogRetentionDays = 30,
EnforceHttps = true,
ValidateSslCertificates = true,
ConnectionTimeoutSeconds = 30,
AutoBackupEnabled = false,
EncryptBackups = true,
BackupRetentionDays = 30
};
await Task.Delay(300);
await OnShowToast.InvokeAsync(("Impostazioni di sicurezza ripristinate", "info"));
StateHasChanged();
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore reset impostazioni");
await OnShowToast.InvokeAsync(("Errore reset impostazioni", "error"));
}
}
private class SecuritySettings
{
public bool LogCredentialAccess { get; set; }
public bool LogDataTransfers { get; set; }
public bool LogFailedOperations { get; set; }
public int LogRetentionDays { get; set; }
public bool EnforceHttps { get; set; }
public bool ValidateSslCertificates { get; set; }
public int ConnectionTimeoutSeconds { get; set; }
public bool AutoBackupEnabled { get; set; }
public bool EncryptBackups { get; set; }
public int BackupRetentionDays { get; set; }
}
}
+200
View File
@@ -0,0 +1,200 @@
@inject ILogger<SystemTab> Logger
<div class="settings-section">
<h4>
<i class="fas fa-server text-primary me-2"></i>
Configurazione Sistema
</h4>
<p class="text-muted">
Impostazioni generali del sistema Data Coupler.
</p>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-database text-info me-2"></i>
Database
</h5>
</div>
<div class="card-body">
<p class="card-text">Configurazione del database principale.</p>
<div class="mb-3">
<label class="form-label">Percorso Database:</label>
<input type="text" class="form-control" value="@GetDatabasePath()" readonly>
<div class="form-text">Percorso del database SQLite di sistema</div>
</div>
<div class="mb-3">
<label class="form-label">Statistiche Database:</label>
<div class="row">
<div class="col-6">
<div class="text-center p-2 bg-light rounded">
<div class="h5 mb-1">@databaseStats.TotalProfiles</div>
<small class="text-muted">Profili</small>
</div>
</div>
<div class="col-6">
<div class="text-center p-2 bg-light rounded">
<div class="h5 mb-1">@databaseStats.TotalCredentials</div>
<small class="text-muted">Credenziali</small>
</div>
</div>
</div>
</div>
<button class="btn btn-outline-primary btn-sm" @onclick="RefreshStats">
<i class="fas fa-refresh me-1"></i>
Aggiorna Statistiche
</button>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-cogs text-warning me-2"></i>
Performance
</h5>
</div>
<div class="card-body">
<p class="card-text">Impostazioni per ottimizzare le performance.</p>
<div class="mb-3">
<label for="batchSize" class="form-label">Dimensione Batch Predefinita:</label>
<input type="number" class="form-control" id="batchSize" @bind="systemSettings.DefaultBatchSize"
min="1" max="1000" step="1">
<div class="form-text">Numero di record da processare per volta</div>
</div>
<div class="mb-3">
<label for="timeout" class="form-label">Timeout Connessioni (secondi):</label>
<input type="number" class="form-control" id="timeout" @bind="systemSettings.DefaultTimeout"
min="10" max="300" step="5">
</div>
<button class="btn btn-primary btn-sm" @onclick="SaveSystemSettings">
<i class="fas fa-save me-1"></i>
Salva Impostazioni
</button>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-info-circle text-success me-2"></i>
Informazioni Sistema
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<strong>Versione Applicazione:</strong><br>
<span class="badge bg-primary">1.0.0</span>
</div>
<div class="col-md-3">
<strong>Framework:</strong><br>
<span class="badge bg-info">.NET 9.0</span>
</div>
<div class="col-md-3">
<strong>Sistema Operativo:</strong><br>
<span class="badge bg-secondary">@Environment.OSVersion.Platform</span>
</div>
<div class="col-md-3">
<strong>Memoria Utilizzata:</strong><br>
<span class="badge bg-warning">@GetMemoryUsage()</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@code {
[Parameter] public EventCallback<(string message, string type)> OnShowToast { get; set; }
private DatabaseStats databaseStats = new();
private SystemSettings systemSettings = new()
{
DefaultBatchSize = 200,
DefaultTimeout = 30
};
protected override async Task OnInitializedAsync()
{
await RefreshStats();
}
private string GetDatabasePath()
{
// Implementazione semplificata - in un'implementazione reale
// questo dovrebbe venire dalla configurazione
var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
return Path.Combine(documentsPath, "DataCoupler", "credentials.db");
}
private string GetMemoryUsage()
{
var workingSet = Environment.WorkingSet;
return $"{workingSet / 1024 / 1024:F1} MB";
}
private async Task RefreshStats()
{
try
{
// Implementazione semplificata - in produzione collegarsi al database
databaseStats = new DatabaseStats
{
TotalProfiles = Random.Shared.Next(5, 50),
TotalCredentials = Random.Shared.Next(3, 20)
};
await OnShowToast.InvokeAsync(("Statistiche aggiornate", "success"));
StateHasChanged();
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore aggiornamento statistiche");
await OnShowToast.InvokeAsync(("Errore aggiornamento statistiche", "error"));
}
}
private async Task SaveSystemSettings()
{
try
{
// Implementazione semplificata - in produzione salvare nel database o file di configurazione
await Task.Delay(500); // Simula operazione di salvataggio
await OnShowToast.InvokeAsync(("Impostazioni salvate con successo", "success"));
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore salvataggio impostazioni");
await OnShowToast.InvokeAsync(("Errore salvataggio impostazioni", "error"));
}
}
private class DatabaseStats
{
public int TotalProfiles { get; set; }
public int TotalCredentials { get; set; }
}
private class SystemSettings
{
public int DefaultBatchSize { get; set; }
public int DefaultTimeout { get; set; }
}
}
@@ -140,19 +140,12 @@ public partial class DataCoupler : ComponentBase
Logger.LogInformation("Autenticazione completata con successo per il servizio REST {ServiceType}", credential.ServiceType);
// Discovery delle entità disponibili
Logger.LogInformation("Iniziando discovery delle entità REST...");
var entities = await currentRestDiscovery.DiscoverEntitiesAsync();
restEntities = entities.Select(e => new RestEntitySummary
{
Name = e.Name,
Label = e.Name, // Use Name as Label since RestEntityInfo doesn't have Label
Description = "" // RestEntityInfo doesn't have Description
}).ToList();
// Discovery delle entità disponibili usando il metodo batch ottimizzato
Logger.LogInformation("Iniziando discovery batch delle entità REST...");
restEntities = await currentRestDiscovery.DiscoverEntitySummariesAsync();
isRestConnected = true;
Logger.LogInformation("Discovery completato: trovate {EntityCount} entità REST", restEntities.Count);
Logger.LogInformation("Discovery batch completato: trovate {EntityCount} entità REST", restEntities.Count);
}
catch (Exception ex)
{
+334
View File
@@ -0,0 +1,334 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using CredentialManager.Models;
namespace Data_Coupler.Models;
/// <summary>
/// Modello per l'export/import completo dei dati di backup
/// </summary>
public class SystemBackupData
{
[JsonPropertyName("metadata")]
public BackupMetadata Metadata { get; set; } = new();
[JsonPropertyName("profiles")]
public List<DataCouplerProfileBackup> Profiles { get; set; } = new();
[JsonPropertyName("credentials")]
public List<CredentialBackup> Credentials { get; set; } = new();
[JsonPropertyName("keyAssociations")]
public List<KeyAssociationBackup> KeyAssociations { get; set; } = new();
[JsonPropertyName("profileSchedules")]
public List<ProfileScheduleBackup> ProfileSchedules { get; set; } = new();
}
/// <summary>
/// Metadati del backup
/// </summary>
public class BackupMetadata
{
[JsonPropertyName("version")]
public string Version { get; set; } = "1.0";
[JsonPropertyName("createdAt")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[JsonPropertyName("createdBy")]
public string? CreatedBy { get; set; }
[JsonPropertyName("applicationVersion")]
public string ApplicationVersion { get; set; } = "1.0.0";
[JsonPropertyName("totalRecords")]
public BackupRecordCount RecordCounts { get; set; } = new();
[JsonPropertyName("description")]
public string? Description { get; set; }
}
/// <summary>
/// Conteggio record nel backup
/// </summary>
public class BackupRecordCount
{
[JsonPropertyName("profiles")]
public int Profiles { get; set; }
[JsonPropertyName("credentials")]
public int Credentials { get; set; }
[JsonPropertyName("keyAssociations")]
public int KeyAssociations { get; set; }
[JsonPropertyName("profileSchedules")]
public int ProfileSchedules { get; set; }
}
/// <summary>
/// Backup di un profilo DataCoupler
/// </summary>
public class DataCouplerProfileBackup
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("sourceType")]
public string SourceType { get; set; } = string.Empty;
[JsonPropertyName("sourceCredentialName")]
public string? SourceCredentialName { get; set; }
[JsonPropertyName("sourceDatabaseName")]
public string? SourceDatabaseName { get; set; }
[JsonPropertyName("sourceSchema")]
public string? SourceSchema { get; set; }
[JsonPropertyName("sourceTable")]
public string? SourceTable { get; set; }
[JsonPropertyName("sourceCustomQuery")]
public string? SourceCustomQuery { get; set; }
[JsonPropertyName("sourceFilePath")]
public string? SourceFilePath { get; set; }
[JsonPropertyName("destinationType")]
public string DestinationType { get; set; } = string.Empty;
[JsonPropertyName("destinationCredentialName")]
public string? DestinationCredentialName { get; set; }
[JsonPropertyName("destinationSchema")]
public string? DestinationSchema { get; set; }
[JsonPropertyName("destinationTable")]
public string? DestinationTable { get; set; }
[JsonPropertyName("destinationEndpoint")]
public string? DestinationEndpoint { get; set; }
[JsonPropertyName("fieldMappings")]
public List<FieldMappingDto>? FieldMappings { get; set; }
[JsonPropertyName("sourceKeyField")]
public string? SourceKeyField { get; set; }
[JsonPropertyName("useRecordAssociations")]
public bool UseRecordAssociations { get; set; }
[JsonPropertyName("createdBy")]
public string? CreatedBy { get; set; }
[JsonPropertyName("createdAt")]
public DateTime CreatedAt { get; set; }
[JsonPropertyName("lastUsedAt")]
public DateTime? LastUsedAt { get; set; }
[JsonPropertyName("isActive")]
public bool IsActive { get; set; } = true;
}
/// <summary>
/// Backup di una credenziale (dati sensibili esclusi per sicurezza)
/// </summary>
public class CredentialBackup
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("type")]
public string Type { get; set; } = string.Empty;
[JsonPropertyName("databaseType")]
public string? DatabaseType { get; set; }
[JsonPropertyName("host")]
public string? Host { get; set; }
[JsonPropertyName("port")]
public int? Port { get; set; }
[JsonPropertyName("databaseName")]
public string? DatabaseName { get; set; }
[JsonPropertyName("username")]
public string? Username { get; set; }
// NOTA: Password, API Keys e Token NON vengono inclusi nel backup per sicurezza
[JsonPropertyName("commandTimeout")]
public int CommandTimeout { get; set; } = 30;
[JsonPropertyName("timeoutSeconds")]
public int TimeoutSeconds { get; set; } = 100;
[JsonPropertyName("ignoreSslErrors")]
public bool IgnoreSslErrors { get; set; } = false;
[JsonPropertyName("restServiceType")]
public string? RestServiceType { get; set; }
[JsonPropertyName("headers")]
public string? Headers { get; set; }
[JsonPropertyName("additionalParameters")]
public string? AdditionalParameters { get; set; }
[JsonPropertyName("createdAt")]
public DateTime CreatedAt { get; set; }
[JsonPropertyName("updatedAt")]
public DateTime? UpdatedAt { get; set; }
[JsonPropertyName("createdBy")]
public string? CreatedBy { get; set; }
[JsonPropertyName("isActive")]
public bool IsActive { get; set; } = true;
}
/// <summary>
/// Backup di un'associazione chiave
/// </summary>
public class KeyAssociationBackup
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("keyValue")]
public string KeyValue { get; set; } = string.Empty;
[JsonPropertyName("sourceKeyField")]
public string SourceKeyField { get; set; } = string.Empty;
[JsonPropertyName("destinationKeyField")]
public string DestinationKeyField { get; set; } = string.Empty;
[JsonPropertyName("destinationEntity")]
public string DestinationEntity { get; set; } = string.Empty;
[JsonPropertyName("destinationId")]
public string DestinationId { get; set; } = string.Empty;
[JsonPropertyName("restCredentialName")]
public string RestCredentialName { get; set; } = string.Empty;
[JsonPropertyName("createdAt")]
public DateTime CreatedAt { get; set; }
[JsonPropertyName("updatedAt")]
public DateTime? UpdatedAt { get; set; }
[JsonPropertyName("lastVerifiedAt")]
public DateTime? LastVerifiedAt { get; set; }
[JsonPropertyName("isActive")]
public bool IsActive { get; set; } = true;
[JsonPropertyName("dataHash")]
public string? DataHash { get; set; }
[JsonPropertyName("sourcesInfo")]
public string? SourcesInfo { get; set; }
[JsonPropertyName("additionalInfo")]
public string? AdditionalInfo { get; set; }
}
/// <summary>
/// Backup di una schedule profilo
/// </summary>
public class ProfileScheduleBackup
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("profileName")]
public string ProfileName { get; set; } = string.Empty;
[JsonPropertyName("scheduleType")]
public string ScheduleType { get; set; } = string.Empty; // "daily", "weekly", "monthly"
[JsonPropertyName("dailyExecutionTime")]
public TimeSpan? DailyExecutionTime { get; set; }
[JsonPropertyName("weeklyDayOfWeek")]
public int? WeeklyDayOfWeek { get; set; } // 0 = Sunday, 1 = Monday, etc.
[JsonPropertyName("monthlyDayOfMonth")]
public int? MonthlyDayOfMonth { get; set; } // 1-31
[JsonPropertyName("isActive")]
public bool IsActive { get; set; } = true;
[JsonPropertyName("isPaused")]
public bool IsPaused { get; set; } = false;
[JsonPropertyName("lastExecuted")]
public DateTime? LastExecuted { get; set; }
[JsonPropertyName("nextExecution")]
public DateTime? NextExecution { get; set; }
[JsonPropertyName("createdAt")]
public DateTime CreatedAt { get; set; }
[JsonPropertyName("createdBy")]
public string? CreatedBy { get; set; }
}
/// <summary>
/// Risultato dell'operazione di backup/restore
/// </summary>
public class BackupOperationResult
{
public bool Success { get; set; }
public string? Message { get; set; }
public TimeSpan Duration { get; set; }
public BackupRecordCount ProcessedCounts { get; set; } = new();
public List<string> Warnings { get; set; } = new();
public List<string> Errors { get; set; } = new();
public string? FilePath { get; set; }
}
/// <summary>
/// Opzioni per l'operazione di backup
/// </summary>
public class BackupOptions
{
public bool IncludeProfiles { get; set; } = true;
public bool IncludeCredentials { get; set; } = true;
public bool IncludeKeyAssociations { get; set; } = true;
public bool IncludeProfileSchedules { get; set; } = true;
public bool IncludeOnlyActiveRecords { get; set; } = true;
public string? Description { get; set; }
public string? CreatedBy { get; set; }
}
/// <summary>
/// Opzioni per l'operazione di restore
/// </summary>
public class RestoreOptions
{
public bool RestoreProfiles { get; set; } = true;
public bool RestoreCredentials { get; set; } = false; // Default false per sicurezza
public bool RestoreKeyAssociations { get; set; } = true;
public bool RestoreProfileSchedules { get; set; } = true;
public bool OverwriteExisting { get; set; } = false;
public bool CreateBackupBeforeRestore { get; set; } = true;
public string? ImportedBy { get; set; }
}
+14 -24
View File
@@ -1812,36 +1812,25 @@ public partial class DataCoupler : ComponentBase
}
/// <summary>
/// Genera un hash SHA256 dei dati dei campi sorgente mappati.
/// Genera un hash SHA256 dei dati del record passato come parametro.
/// Utilizzato per rilevare cambiamenti nei dati e ottimizzare il trasferimento.
/// Include anche una signature dei campi mappati per rilevare cambi di configurazione.
/// Calcola l'hash SOLO sui campi presenti nel record, in ordine alfabetico.
/// </summary>
private string GenerateDataHash(Dictionary<string, object> record)
{
try
{
// Raccoglie i valori dei campi mappati in ordine alfabetico per garantire consistenza
var mappedFields = fieldMappings.Keys.OrderBy(k => k).ToList();
var valuesForHash = new List<string>();
// PRIMO: Aggiungi la signature dei mapping per rilevare cambi di configurazione
var mappingSignature = string.Join(",", fieldMappings.OrderBy(m => m.Key).Select(m => $"{m.Key}->{m.Value}"));
valuesForHash.Add($"MAPPING_SIGNATURE={mappingSignature}");
// Ordina le chiavi alfabeticamente per garantire consistenza
var orderedKeys = record.Keys.OrderBy(k => k).ToList();
// SECONDO: Aggiungi i valori dei dati per ogni campo mappato
foreach (var sourceField in mappedFields)
// Aggiungi i valori dei dati per ogni campo presente nel record
foreach (var key in orderedKeys)
{
if (record.ContainsKey(sourceField))
{
var value = record[sourceField];
var normalizedValue = value?.ToString()?.Trim() ?? "";
valuesForHash.Add($"{sourceField}={normalizedValue}");
}
else
{
// Se il campo non è presente nel record, aggiungi una stringa vuota
valuesForHash.Add($"{sourceField}=");
}
var value = record[key];
var normalizedValue = value?.ToString()?.Trim() ?? "";
valuesForHash.Add($"{key}={normalizedValue}");
}
// Combina tutti i valori in una stringa unica
@@ -1855,7 +1844,7 @@ public partial class DataCoupler : ComponentBase
var hashBytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(combinedData));
var hashString = Convert.ToHexString(hashBytes);
Logger.LogDebug("Hash SHA256 generato: {Hash} (include signature mapping)", hashString);
Logger.LogDebug("Hash SHA256 generato: {Hash} per {FieldCount} campi", hashString, orderedKeys.Count);
return hashString;
}
}
@@ -2578,7 +2567,8 @@ public partial class DataCoupler : ComponentBase
// Genera la chiave sorgente e l'hash dei dati per questo record (operazioni locali, thread-safe)
var sourceKey = GenerateSourceKey(record);
var currentDataHash = GenerateDataHash(record);
// ✅ Calcola l'hash SOLO sui dati trasformati/mappati che vengono effettivamente trasferiti
var currentDataHash = GenerateDataHash(restData);
// Analizza le associazioni per capire se aggiornare, creare o saltare
if (currentUseRecordAssociations && !string.IsNullOrEmpty(sourceKey))
@@ -2738,8 +2728,8 @@ public partial class DataCoupler : ComponentBase
if (useRecordAssociations && !string.IsNullOrEmpty(transferResult.EntityId))
{
// IMPORTANTE: Non awaita qui, solo crea il task per esecuzione parallela
// Genera l'hash per questo record per salvarlo nell'associazione
var dataHashForAssociation = GenerateDataHash(originalData.originalRecord);
// Genera l'hash SOLO sui dati trasformati/mappati che sono stati effettivamente trasferiti
var dataHashForAssociation = GenerateDataHash(originalData.transformedData);
var associationTask = CreateAssociationAsync(originalData.originalRecord, transferResult.EntityId, originalData.recordNumber, dataHashForAssociation);
createAssociationTasks.Add(associationTask);
}
+350
View File
@@ -0,0 +1,350 @@
@page "/scheduling"
@using CredentialManager.Models
@using CredentialManager.Services
@using Data_Coupler.Services
<PageTitle>Schedulazione Profili</PageTitle>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h3><i class="fas fa-clock"></i> Schedulazione Profili</h3>
<div>
<a href="/scheduling/history" class="btn btn-outline-info me-2">
<i class="fas fa-history"></i> Storico Esecuzioni
</a>
<button class="btn btn-success" @onclick="ShowCreateModal">
<i class="fas fa-plus"></i> Nuova Schedulazione
</button>
</div>
</div>
@if (schedules == null)
{
<div class="d-flex justify-content-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Caricamento...</span>
</div>
</div>
}
else if (!schedules.Any())
{
<div class="alert alert-info">
<i class="fas fa-info-circle"></i> Nessuna schedulazione configurata.
<button class="btn btn-link p-0 ms-2" @onclick="ShowCreateModal">
Crea la prima schedulazione
</button>
</div>
}
else
{
<div class="row">
@foreach (var schedule in schedules.OrderBy(s => s.NextExecutionTime))
{
<div class="col-md-6 col-lg-4 mb-3">
<div class="card h-100 @(schedule.IsEnabled ? "border-success" : "border-secondary")">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fas fa-@(schedule.ScheduleType switch { "once" => "clock", "interval" => "redo", "daily" => "calendar-day", "weekly" => "calendar-week", "monthly" => "calendar", _ => "clock" })"></i>
@schedule.Name
</h6>
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i>
</button>
<ul class="dropdown-menu">
<li><button class="dropdown-item" @onclick="() => ShowEditModal(schedule)">
<i class="fas fa-edit"></i> Modifica
</button></li>
<li><button class="dropdown-item" @onclick="() => ExecuteScheduleManually(schedule.Id)">
<i class="fas fa-play"></i> Esegui Ora
</button></li>
<li><hr class="dropdown-divider"></li>
<li><button class="dropdown-item text-danger" @onclick="() => DeleteSchedule(schedule.Id)">
<i class="fas fa-trash"></i> Elimina
</button></li>
</ul>
</div>
</div>
<div class="card-body">
<p class="card-text text-muted mb-2">
<strong>Profilo:</strong> @schedule.Profile?.Name
</p>
@if (!string.IsNullOrEmpty(schedule.Description))
{
<p class="card-text small">@schedule.Description</p>
}
<div class="mb-2">
<small class="text-muted">
<strong>Tipo:</strong> @schedule.GetScheduleDescription()
</small>
</div>
<!-- Prossima esecuzione -->
@if (schedule.NextExecutionTime.HasValue)
{
<div class="mb-2">
<small class="text-info">
<i class="fas fa-clock"></i>
<strong>Prossima esecuzione:</strong><br>
@schedule.NextExecutionTime.Value.ToString("dd/MM/yyyy HH:mm")
</small>
</div>
}
<!-- Ultima esecuzione -->
@if (schedule.LastExecutionTime.HasValue)
{
<div class="mb-2">
<small class="text-muted">
<i class="fas fa-history"></i>
<strong>Ultima esecuzione:</strong><br>
@schedule.LastExecutionTime.Value.ToString("dd/MM/yyyy HH:mm")
</small>
</div>
}
<!-- Status ultima esecuzione -->
@if (!string.IsNullOrEmpty(schedule.LastExecutionStatus))
{
<div class="mb-2">
<span class="badge bg-@(schedule.LastExecutionStatus switch { "success" => "success", "failed" => "danger", "running" => "primary", _ => "secondary" })">
@schedule.LastExecutionStatus.ToUpper()
@if (schedule.LastExecutionRecordCount.HasValue)
{
<text> (@schedule.LastExecutionRecordCount record)</text>
}
</span>
</div>
}
</div>
<div class="card-footer d-flex justify-content-between align-items-center">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox"
checked="@schedule.IsEnabled"
@onchange="(e) => ToggleScheduleEnabled(schedule.Id, (bool)e.Value!)">
<label class="form-check-label small">
@(schedule.IsEnabled ? "Attiva" : "Disattivata")
</label>
</div>
<small class="text-muted">
Esecuzioni: @schedule.ExecutionCount
</small>
</div>
</div>
</div>
}
</div>
}
</div>
</div>
</div>
<!-- Modal per creazione/modifica schedulazione -->
<div class="modal fade" id="scheduleModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
@(editingSchedule?.Id > 0 ? "Modifica Schedulazione" : "Nuova Schedulazione")
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
@if (editingSchedule != null)
{
<EditForm Model="editingSchedule" OnValidSubmit="SaveSchedule">
<DataAnnotationsValidator />
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Nome *</label>
<InputText @bind-Value="editingSchedule.Name" class="form-control" />
<ValidationMessage For="() => editingSchedule.Name" />
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Profilo da eseguire *</label>
<InputSelect @bind-Value="editingSchedule.ProfileId" class="form-select" @onchange="OnProfileSelectionChanged">
<option value="0">-- Seleziona Profilo --</option>
@if (availableProfiles != null)
{
@foreach (var profile in availableProfiles.Where(p => p.SourceType != "file"))
{
<option value="@profile.Id">@profile.Name</option>
}
}
</InputSelect>
<ValidationMessage For="() => editingSchedule.ProfileId" />
@if (availableProfiles?.Any(p => p.SourceType == "file") == true)
{
<small class="form-text text-muted">
⚠️ I profili con file come sorgente sono esclusi dalle schedulazioni per motivi di sicurezza.
</small>
}
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Descrizione</label>
<InputTextArea @bind-Value="editingSchedule.Description" class="form-control" rows="2" />
</div>
@* Sezione override database *@
@if (selectedProfile != null && (selectedProfile.SourceType == "database" || selectedProfile.DestinationType == "database"))
{
<div class="card mb-3">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-database"></i> Override Database
</h6>
</div>
<div class="card-body">
<div class="row">
@if (selectedProfile.SourceType == "database")
{
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Database Sorgente (Opzionale)</label>
<InputText @bind-Value="editingSchedule.SourceDatabaseOverride" class="form-control" />
<small class="form-text text-muted">
Lascia vuoto per usare il database specificato nella credenziale.
</small>
</div>
</div>
}
@if (selectedProfile.DestinationType == "database")
{
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Database Destinazione (Opzionale)</label>
<InputText @bind-Value="editingSchedule.DestinationDatabaseOverride" class="form-control" />
<small class="form-text text-muted">
Lascia vuoto per usare il database specificato nella credenziale.
</small>
</div>
</div>
}
</div>
</div>
</div>
}
<div class="mb-3">
<label class="form-label">Tipo di Schedulazione *</label>
<InputSelect @bind-Value="editingSchedule.ScheduleType" class="form-select" @onchange="OnScheduleTypeChanged">
<option value="">-- Seleziona Tipo --</option>
<option value="once">Una volta</option>
<option value="interval">Intervallo Personalizzato</option>
<option value="daily">Giornaliera</option>
<option value="weekly">Settimanale</option>
<option value="monthly">Mensile</option>
</InputSelect>
<ValidationMessage For="() => editingSchedule.ScheduleType" />
</div>
@if (editingSchedule.ScheduleType == "once")
{
<div class="mb-3">
<label class="form-label">Data e Ora di Esecuzione *</label>
<input type="datetime-local" @bind="editingSchedule.ScheduledDateTime" class="form-control" />
</div>
}
else if (editingSchedule.ScheduleType == "interval")
{
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Intervallo *</label>
<InputNumber @bind-Value="editingSchedule.IntervalValue" class="form-control" min="1" />
<small class="form-text text-muted">Numero di unità temporali</small>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Unità di Tempo *</label>
<InputSelect @bind-Value="editingSchedule.IntervalUnit" class="form-select">
<option value="">-- Seleziona Unità --</option>
<option value="seconds">Secondi</option>
<option value="minutes">Minuti</option>
<option value="hours">Ore</option>
<option value="days">Giorni</option>
<option value="weeks">Settimane</option>
<option value="months">Mesi</option>
</InputSelect>
<small class="form-text text-muted">Frequenza di ripetizione</small>
</div>
</div>
</div>
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
<strong>Anteprima:</strong> @GetIntervalPreview()
</div>
}
else if (!string.IsNullOrEmpty(editingSchedule.ScheduleType) && editingSchedule.ScheduleType != "once" && editingSchedule.ScheduleType != "interval")
{
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Ora di Esecuzione *</label>
<InputText @bind-Value="editingSchedule.DailyTime" class="form-control" placeholder="HH:mm" />
<small class="form-text text-muted">Formato 24 ore (es: 14:30)</small>
</div>
</div>
@if (editingSchedule.ScheduleType == "weekly")
{
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Giorno della Settimana *</label>
<InputSelect @bind-Value="editingSchedule.DayOfWeek" class="form-select">
<option value="">-- Seleziona --</option>
<option value="0">Domenica</option>
<option value="1">Lunedì</option>
<option value="2">Martedì</option>
<option value="3">Mercoledì</option>
<option value="4">Giovedì</option>
<option value="5">Venerdì</option>
<option value="6">Sabato</option>
</InputSelect>
</div>
</div>
}
else if (editingSchedule.ScheduleType == "monthly")
{
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Giorno del Mese *</label>
<InputNumber @bind-Value="editingSchedule.DayOfMonth" class="form-control" min="1" max="31" />
<small class="form-text text-muted">1-31</small>
</div>
</div>
}
</div>
}
<div class="form-check mb-3">
<InputCheckbox @bind-Value="editingSchedule.IsEnabled" class="form-check-input" />
<label class="form-check-label">
Schedulazione attiva
</label>
</div>
<div class="d-flex justify-content-end gap-2">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Salva
</button>
</div>
</EditForm>
}
</div>
</div>
</div>
</div>
+444
View File
@@ -0,0 +1,444 @@
using CredentialManager.Models;
using CredentialManager.Services;
using Data_Coupler.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
namespace Data_Coupler.Pages;
public partial class Scheduling : ComponentBase
{
[Inject] private IProfileScheduleService ScheduleService { get; set; } = null!;
[Inject] private IDataCouplerProfileService ProfileService { get; set; } = null!;
[Inject] private IDataTransferService DataTransferService { get; set; } = null!;
[Inject] private IJSRuntime JSRuntime { get; set; } = null!;
[Inject] private ILogger<Scheduling> Logger { get; set; } = null!;
protected List<ProfileSchedule>? schedules;
protected List<DataCouplerProfile>? availableProfiles;
protected ProfileSchedule? editingSchedule;
protected DataCouplerProfile? selectedProfile;
protected bool isExecuting = false;
protected List<ScheduleExecutionHistory>? executionHistory;
protected override async Task OnInitializedAsync()
{
await LoadSchedules();
await LoadAvailableProfiles();
}
protected async Task LoadSchedules()
{
try
{
schedules = await ScheduleService.GetAllSchedulesAsync();
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nel caricamento delle schedulazioni");
await ShowErrorMessage("Errore nel caricamento delle schedulazioni: " + ex.Message);
}
}
protected async Task LoadAvailableProfiles()
{
try
{
availableProfiles = await ScheduleService.GetAvailableProfilesAsync();
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nel caricamento dei profili disponibili");
}
}
protected async Task ShowCreateModal()
{
editingSchedule = new ProfileSchedule
{
Name = "",
IsEnabled = true,
ScheduleType = "",
DailyTime = "09:00",
IntervalValue = 5,
IntervalUnit = "minutes"
};
selectedProfile = null;
await ShowModal();
}
protected async Task ShowEditModal(ProfileSchedule schedule)
{
editingSchedule = new ProfileSchedule
{
Id = schedule.Id,
Name = schedule.Name,
Description = schedule.Description,
ProfileId = schedule.ProfileId,
IsEnabled = schedule.IsEnabled,
ScheduleType = schedule.ScheduleType,
ScheduledDateTime = schedule.ScheduledDateTime,
DailyTime = schedule.DailyTime,
DayOfWeek = schedule.DayOfWeek,
DayOfMonth = schedule.DayOfMonth,
IntervalValue = schedule.IntervalValue,
IntervalUnit = schedule.IntervalUnit,
SourceDatabaseOverride = schedule.SourceDatabaseOverride,
DestinationDatabaseOverride = schedule.DestinationDatabaseOverride
};
// Imposta il profilo selezionato per mostrare i campi di override
selectedProfile = availableProfiles?.FirstOrDefault(p => p.Id == schedule.ProfileId);
await ShowModal();
}
protected async Task ShowModal()
{
StateHasChanged();
await Task.Delay(100);
try
{
// Proviamo prima con l'approccio Bootstrap standard
await JSRuntime.InvokeVoidAsync("eval",
"if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { " +
"var modal = new bootstrap.Modal(document.getElementById('scheduleModal')); " +
"modal.show(); " +
"} else { " +
"document.getElementById('scheduleModal').style.display = 'block'; " +
"document.getElementById('scheduleModal').classList.add('show'); " +
"}");
}
catch (Exception)
{
// Fallback: mostra il modal manualmente
await JSRuntime.InvokeVoidAsync("eval",
"var modal = document.getElementById('scheduleModal');" +
"modal.style.display = 'block';" +
"modal.classList.add('show');" +
"document.body.classList.add('modal-open');" +
"var backdrop = document.createElement('div');" +
"backdrop.className = 'modal-backdrop fade show';" +
"document.body.appendChild(backdrop);");
}
}
protected async Task HideModal()
{
try
{
await JSRuntime.InvokeVoidAsync("eval",
"if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { " +
"var modalElement = document.getElementById('scheduleModal'); " +
"var modal = bootstrap.Modal.getInstance(modalElement); " +
"if (modal) modal.hide(); " +
"} else { " +
"document.getElementById('scheduleModal').style.display = 'none'; " +
"document.getElementById('scheduleModal').classList.remove('show'); " +
"document.body.classList.remove('modal-open'); " +
"var backdrop = document.querySelector('.modal-backdrop'); " +
"if (backdrop) backdrop.remove(); " +
"}");
}
catch (Exception)
{
// Fallback: nascondi il modal manualmente
await JSRuntime.InvokeVoidAsync("eval",
"var modal = document.getElementById('scheduleModal');" +
"modal.style.display = 'none';" +
"modal.classList.remove('show');" +
"document.body.classList.remove('modal-open');" +
"var backdrop = document.querySelector('.modal-backdrop');" +
"if (backdrop) backdrop.remove();");
}
}
protected async Task SaveSchedule()
{
if (editingSchedule == null) return;
try
{
if (editingSchedule.Id == 0)
{
await ScheduleService.CreateScheduleAsync(editingSchedule);
await ShowSuccessMessage("Schedulazione creata con successo!");
}
else
{
await ScheduleService.UpdateScheduleAsync(editingSchedule);
await ShowSuccessMessage("Schedulazione aggiornata con successo!");
}
await CloseModal();
await LoadSchedules();
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nel salvataggio della schedulazione");
await ShowErrorMessage("Errore nel salvataggio: " + ex.Message);
}
}
protected async Task DeleteSchedule(int scheduleId)
{
var confirmed = await JSRuntime.InvokeAsync<bool>("confirm", "Sei sicuro di voler eliminare questa schedulazione?");
if (!confirmed) return;
try
{
var success = await ScheduleService.DeleteScheduleAsync(scheduleId);
if (success)
{
await ShowSuccessMessage("Schedulazione eliminata con successo!");
await LoadSchedules();
}
else
{
await ShowErrorMessage("Schedulazione non trovata.");
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nell'eliminazione della schedulazione {ScheduleId}", scheduleId);
await ShowErrorMessage("Errore nell'eliminazione: " + ex.Message);
}
}
protected async Task ToggleScheduleEnabled(int scheduleId, bool enabled)
{
try
{
var schedule = schedules?.FirstOrDefault(s => s.Id == scheduleId);
if (schedule != null)
{
schedule.IsEnabled = enabled;
await ScheduleService.UpdateScheduleAsync(schedule);
Logger.LogInformation("Schedulazione {ScheduleId} {Status}", scheduleId, enabled ? "attivata" : "disattivata");
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nel cambio stato schedulazione {ScheduleId}", scheduleId);
await ShowErrorMessage("Errore nel cambio stato: " + ex.Message);
await LoadSchedules(); // Ricarica per ripristinare lo stato
}
}
protected async Task ExecuteScheduleManually(int scheduleId)
{
if (isExecuting) return;
var confirmed = await JSRuntime.InvokeAsync<bool>("confirm", "Eseguire immediatamente questa schedulazione?");
if (!confirmed) return;
isExecuting = true;
ScheduleExecutionHistory? executionHistory = null;
StateHasChanged();
try
{
var schedule = schedules?.FirstOrDefault(s => s.Id == scheduleId);
if (schedule?.Profile == null)
{
await ShowErrorMessage("Schedulazione o profilo non trovato.");
return;
}
Logger.LogInformation("Esecuzione manuale schedulazione {ScheduleId} - {ScheduleName}", scheduleId, schedule.Name);
// Crea record nello storico
executionHistory = new ScheduleExecutionHistory
{
ScheduleId = scheduleId,
ProfileId = schedule.ProfileId,
ProfileName = schedule.Profile.Name,
StartTime = DateTime.Now,
Status = "running",
TriggerType = "manual",
TriggeredBy = Environment.UserName,
SourceType = schedule.Profile.SourceType,
DestinationType = schedule.Profile.DestinationType,
SourceInfo = schedule.SourceDatabaseOverride != null ? $"Database Override: {schedule.SourceDatabaseOverride}" : null,
DestinationInfo = schedule.DestinationDatabaseOverride != null ? $"Database Override: {schedule.DestinationDatabaseOverride}" : null,
Message = "Esecuzione manuale avviata"
};
executionHistory = await ScheduleService.CreateExecutionHistoryAsync(executionHistory);
// Aggiorna lo status della schedulazione
await ScheduleService.UpdateExecutionStatusAsync(scheduleId, "running", "Esecuzione manuale avviata");
await LoadSchedules();
// Esegui il trasferimento dati con override database se specificati
var result = await DataTransferService.ExecuteProfileAsync(
schedule.Profile,
schedule.SourceDatabaseOverride,
schedule.DestinationDatabaseOverride);
// Aggiorna lo storico con il risultato
executionHistory.EndTime = DateTime.Now;
executionHistory.Status = result.IsSuccess ? "success" : "failed";
executionHistory.RecordsProcessed = result.RecordsProcessed;
executionHistory.Message = result.IsSuccess
? $"Esecuzione completata con successo. {result.RecordsProcessed} record elaborati in {result.Duration.TotalSeconds:F2} secondi."
: $"Esecuzione fallita: {result.ErrorMessage}";
if (!result.IsSuccess)
{
executionHistory.ErrorDetails = string.Join(Environment.NewLine, result.ErrorDetails);
}
// Aggiungi informazioni aggiuntive se disponibili
if (result.AdditionalInfo.Any())
{
executionHistory.AdditionalInfo = System.Text.Json.JsonSerializer.Serialize(result.AdditionalInfo);
}
await ScheduleService.UpdateExecutionHistoryAsync(executionHistory);
// Aggiorna lo status della schedulazione
var status = result.IsSuccess ? "success" : "failed";
var message = result.IsSuccess
? $"Esecuzione completata con successo. {result.RecordsProcessed} record elaborati."
: $"Esecuzione fallita: {result.ErrorMessage}";
await ScheduleService.UpdateExecutionStatusAsync(scheduleId, status, message, result.RecordsProcessed);
if (result.IsSuccess)
{
await ShowSuccessMessage($"Schedulazione eseguita con successo! {result.RecordsProcessed} record elaborati in {result.Duration.TotalSeconds:F2} secondi.");
}
else
{
await ShowErrorMessage($"Errore durante l'esecuzione: {result.ErrorMessage}");
}
await LoadSchedules();
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nell'esecuzione manuale schedulazione {ScheduleId}", scheduleId);
// Aggiorna lo storico in caso di eccezione
if (executionHistory != null)
{
executionHistory.EndTime = DateTime.Now;
executionHistory.Status = "failed";
executionHistory.Message = $"Errore durante l'esecuzione: {ex.Message}";
executionHistory.ErrorDetails = ex.ToString();
await ScheduleService.UpdateExecutionHistoryAsync(executionHistory);
}
await ScheduleService.UpdateExecutionStatusAsync(scheduleId, "failed", $"Errore: {ex.Message}");
await ShowErrorMessage("Errore nell'esecuzione: " + ex.Message);
}
finally
{
isExecuting = false;
StateHasChanged();
}
}
protected void OnProfileSelectionChanged(ChangeEventArgs e)
{
if (editingSchedule != null && int.TryParse(e.Value?.ToString(), out int profileId))
{
editingSchedule.ProfileId = profileId;
selectedProfile = availableProfiles?.FirstOrDefault(p => p.Id == profileId);
// Reset override database quando cambia profilo
editingSchedule.SourceDatabaseOverride = null;
editingSchedule.DestinationDatabaseOverride = null;
}
else
{
selectedProfile = null;
}
StateHasChanged();
}
protected void OnScheduleTypeChanged(ChangeEventArgs e)
{
if (editingSchedule != null)
{
editingSchedule.ScheduleType = e.Value?.ToString() ?? "";
// Reset campi quando cambia il tipo
if (editingSchedule.ScheduleType != "once")
{
editingSchedule.ScheduledDateTime = null;
}
if (editingSchedule.ScheduleType != "weekly")
{
editingSchedule.DayOfWeek = null;
}
if (editingSchedule.ScheduleType != "monthly")
{
editingSchedule.DayOfMonth = null;
}
if (editingSchedule.ScheduleType != "interval")
{
editingSchedule.IntervalValue = null;
editingSchedule.IntervalUnit = null;
}
else
{
// Imposta valori predefiniti per interval
editingSchedule.IntervalValue ??= 5;
editingSchedule.IntervalUnit ??= "minutes";
}
}
StateHasChanged();
}
protected string GetIntervalPreview()
{
if (editingSchedule == null || !editingSchedule.IntervalValue.HasValue || string.IsNullOrEmpty(editingSchedule.IntervalUnit))
{
return "Configura intervallo e unità di tempo";
}
var value = editingSchedule.IntervalValue.Value;
var unit = editingSchedule.IntervalUnit;
var unitName = unit switch
{
"seconds" => value == 1 ? "secondo" : "secondi",
"minutes" => value == 1 ? "minuto" : "minuti",
"hours" => value == 1 ? "ora" : "ore",
"days" => value == 1 ? "giorno" : "giorni",
"weeks" => value == 1 ? "settimana" : "settimane",
"months" => value == 1 ? "mese" : "mesi",
_ => unit
};
return $"Esecuzione ogni {value} {unitName}";
}
private async Task CloseModal()
{
await HideModal();
editingSchedule = null;
selectedProfile = null;
}
private async Task ShowSuccessMessage(string message)
{
await JSRuntime.InvokeVoidAsync("alert", message);
}
private async Task ShowErrorMessage(string message)
{
await JSRuntime.InvokeVoidAsync("alert", "Errore: " + message);
}
}
+256
View File
@@ -0,0 +1,256 @@
@page "/scheduling/history"
@using CredentialManager.Models
@using CredentialManager.Services
<PageTitle>Storico Esecuzioni</PageTitle>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h3><i class="fas fa-history"></i> Storico Esecuzioni Schedulazioni</h3>
<div>
<button class="btn btn-outline-secondary me-2" @onclick="LoadHistory">
<i class="fas fa-sync-alt"></i> Aggiorna
</button>
<a href="/scheduling" class="btn btn-primary">
<i class="fas fa-arrow-left"></i> Torna alle Schedulazioni
</a>
</div>
</div>
@if (executionHistory == null)
{
<div class="d-flex justify-content-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Caricamento...</span>
</div>
</div>
}
else if (!executionHistory.Any())
{
<div class="alert alert-info">
<i class="fas fa-info-circle"></i> Nessuna esecuzione trovata nello storico.
</div>
}
else
{
<div class="row mb-3">
<div class="col-md-6">
<div class="card bg-light">
<div class="card-body">
<h6 class="card-title">
<i class="fas fa-chart-bar"></i> Statistiche
</h6>
<div class="row text-center">
<div class="col-4">
<div class="fs-5 text-success">@executionHistory.Count(e => e.Status == "success")</div>
<small class="text-muted">Successo</small>
</div>
<div class="col-4">
<div class="fs-5 text-danger">@executionHistory.Count(e => e.Status == "failed")</div>
<small class="text-muted">Fallite</small>
</div>
<div class="col-4">
<div class="fs-5 text-primary">@executionHistory.Count(e => e.Status == "running")</div>
<small class="text-muted">In corso</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card bg-light">
<div class="card-body">
<h6 class="card-title">
<i class="fas fa-database"></i> Record Processati
</h6>
<div class="text-center">
<div class="fs-4 text-info">@executionHistory.Where(e => e.Status == "success").Sum(e => e.RecordsProcessed)</div>
<small class="text-muted">Totale record elaborati con successo</small>
</div>
</div>
</div>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Data/Ora Inizio</th>
<th>Profilo</th>
<th>Schedulazione</th>
<th>Durata</th>
<th>Status</th>
<th>Record</th>
<th>Trigger</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
@foreach (var execution in executionHistory.OrderByDescending(e => e.StartTime))
{
<tr class="@(execution.Status == "success" ? "table-success" : execution.Status == "failed" ? "table-danger" : execution.Status == "running" ? "table-info" : "")">
<td>
<div>@execution.StartTime.ToString("dd/MM/yyyy HH:mm:ss")</div>
@if (execution.EndTime.HasValue)
{
<small class="text-muted">Fine: @execution.EndTime.Value.ToString("HH:mm:ss")</small>
}
</td>
<td>
<div class="fw-bold">@execution.ProfileName</div>
@if (!string.IsNullOrEmpty(execution.SourceInfo) || !string.IsNullOrEmpty(execution.DestinationInfo))
{
<small class="text-muted">
@if (!string.IsNullOrEmpty(execution.SourceInfo))
{
<div>S: @execution.SourceInfo</div>
}
@if (!string.IsNullOrEmpty(execution.DestinationInfo))
{
<div>D: @execution.DestinationInfo</div>
}
</small>
}
</td>
<td>
<div>@execution.Schedule?.Name</div>
<small class="text-muted">ID: @execution.ScheduleId</small>
</td>
<td>
@if (execution.Duration.HasValue)
{
<span class="badge bg-secondary">@FormatDuration(execution.Duration.Value)</span>
}
else if (execution.Status == "running")
{
<span class="badge bg-primary">In corso...</span>
}
else
{
<span class="text-muted">-</span>
}
</td>
<td>
<span class="badge bg-@(execution.Status switch { "success" => "success", "failed" => "danger", "running" => "primary", "cancelled" => "warning", _ => "secondary" })">
@execution.GetStatusDisplayText()
</span>
</td>
<td>
<div class="fw-bold">@execution.RecordsProcessed</div>
@if (execution.RecordsWithErrors.HasValue && execution.RecordsWithErrors.Value > 0)
{
<small class="text-warning">@execution.RecordsWithErrors errori</small>
}
</td>
<td>
<div>
<span class="badge bg-@(execution.TriggerType == "manual" ? "info" : "success")">
@execution.TriggerType.ToUpper()
</span>
</div>
@if (!string.IsNullOrEmpty(execution.TriggeredBy))
{
<small class="text-muted">@execution.TriggeredBy</small>
}
</td>
<td>
<button class="btn btn-sm btn-outline-info" @onclick="() => ShowExecutionDetails(execution)">
<i class="fas fa-eye"></i>
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
</div>
</div>
<!-- Modal per dettagli esecuzione -->
<div class="modal fade" id="executionDetailModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-info-circle"></i> Dettagli Esecuzione
@if (selectedExecution != null)
{
<span> - @selectedExecution.ProfileName</span>
}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
@if (selectedExecution != null)
{
<div class="row">
<div class="col-md-6">
<h6>Informazioni Generali</h6>
<table class="table table-sm">
<tr><th>Data Inizio:</th><td>@selectedExecution.StartTime.ToString("dd/MM/yyyy HH:mm:ss")</td></tr>
<tr><th>Data Fine:</th><td>@(selectedExecution.EndTime?.ToString("dd/MM/yyyy HH:mm:ss") ?? "In corso")</td></tr>
<tr><th>Durata:</th><td>@(selectedExecution.Duration?.ToString(@"hh\:mm\:ss") ?? "N/A")</td></tr>
<tr><th>Status:</th><td><span class="badge bg-@(selectedExecution.Status switch { "success" => "success", "failed" => "danger", "running" => "primary", "cancelled" => "warning", _ => "secondary" })">@selectedExecution.GetStatusDisplayText()</span></td></tr>
<tr><th>Trigger:</th><td>@selectedExecution.TriggerType (@selectedExecution.TriggeredBy)</td></tr>
<tr><th>Record Processati:</th><td>@selectedExecution.RecordsProcessed</td></tr>
@if (selectedExecution.RecordsWithErrors.HasValue)
{
<tr><th>Record con Errori:</th><td class="text-warning">@selectedExecution.RecordsWithErrors</td></tr>
}
</table>
</div>
<div class="col-md-6">
<h6>Configurazione</h6>
<table class="table table-sm">
<tr><th>Schedulazione:</th><td>@selectedExecution.Schedule?.Name</td></tr>
<tr><th>Profilo:</th><td>@selectedExecution.ProfileName (ID: @selectedExecution.ProfileId)</td></tr>
<tr><th>Tipo Sorgente:</th><td>@selectedExecution.SourceType</td></tr>
<tr><th>Tipo Destinazione:</th><td>@selectedExecution.DestinationType</td></tr>
@if (!string.IsNullOrEmpty(selectedExecution.SourceInfo))
{
<tr><th>Info Sorgente:</th><td>@selectedExecution.SourceInfo</td></tr>
}
@if (!string.IsNullOrEmpty(selectedExecution.DestinationInfo))
{
<tr><th>Info Destinazione:</th><td>@selectedExecution.DestinationInfo</td></tr>
}
</table>
</div>
</div>
@if (!string.IsNullOrEmpty(selectedExecution.Message))
{
<h6>Messaggio</h6>
<div class="alert alert-@(selectedExecution.Status == "success" ? "success" : selectedExecution.Status == "failed" ? "danger" : "info")">
@selectedExecution.Message
</div>
}
@if (!string.IsNullOrEmpty(selectedExecution.ErrorDetails))
{
<h6>Dettagli Errori</h6>
<div class="alert alert-danger">
<pre style="white-space: pre-wrap; font-size: 0.85em;">@selectedExecution.ErrorDetails</pre>
</div>
}
@if (!string.IsNullOrEmpty(selectedExecution.AdditionalInfo))
{
<h6>Informazioni Aggiuntive</h6>
<div class="alert alert-light">
<pre style="white-space: pre-wrap; font-size: 0.85em;">@selectedExecution.AdditionalInfo</pre>
</div>
}
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Chiudi</button>
</div>
</div>
</div>
</div>
@@ -0,0 +1,152 @@
using CredentialManager.Models;
using CredentialManager.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
namespace Data_Coupler.Pages;
public partial class SchedulingHistory : ComponentBase
{
[Inject] private IProfileScheduleService ScheduleService { get; set; } = null!;
[Inject] private IJSRuntime JSRuntime { get; set; } = null!;
[Inject] private ILogger<SchedulingHistory> Logger { get; set; } = null!;
protected List<ScheduleExecutionHistory>? executionHistory;
protected ScheduleExecutionHistory? selectedExecution;
protected bool isLoading = false;
protected override async Task OnInitializedAsync()
{
await LoadHistory();
}
protected async Task LoadHistory()
{
if (isLoading) return;
isLoading = true;
StateHasChanged();
try
{
// Carica le ultime 100 esecuzioni
executionHistory = await ScheduleService.GetRecentExecutionsAsync(100);
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nel caricamento dello storico delle esecuzioni");
await ShowErrorMessage("Errore nel caricamento dello storico: " + ex.Message);
}
finally
{
isLoading = false;
StateHasChanged();
}
}
protected async Task ShowExecutionDetails(ScheduleExecutionHistory execution)
{
try
{
// Carica i dettagli completi dell'esecuzione
selectedExecution = await ScheduleService.GetExecutionByIdAsync(execution.Id);
if (selectedExecution != null)
{
await ShowModal();
}
else
{
await ShowErrorMessage("Dettagli dell'esecuzione non trovati.");
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nel caricamento dei dettagli esecuzione {ExecutionId}", execution.Id);
await ShowErrorMessage("Errore nel caricamento dei dettagli: " + ex.Message);
}
}
protected async Task ShowModal()
{
StateHasChanged();
await Task.Delay(100);
try
{
// Proviamo prima con l'approccio Bootstrap standard
await JSRuntime.InvokeVoidAsync("eval",
"if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { " +
"var modal = new bootstrap.Modal(document.getElementById('executionDetailModal')); " +
"modal.show(); " +
"} else { " +
"document.getElementById('executionDetailModal').style.display = 'block'; " +
"document.getElementById('executionDetailModal').classList.add('show'); " +
"}");
}
catch (Exception)
{
// Fallback: mostra il modal manualmente
await JSRuntime.InvokeVoidAsync("eval",
"var modal = document.getElementById('executionDetailModal');" +
"modal.style.display = 'block';" +
"modal.classList.add('show');" +
"document.body.classList.add('modal-open');" +
"var backdrop = document.createElement('div');" +
"backdrop.className = 'modal-backdrop fade show';" +
"document.body.appendChild(backdrop);");
}
}
protected string FormatDuration(TimeSpan duration)
{
if (duration.TotalHours >= 1)
{
return duration.ToString(@"h\:mm\:ss");
}
else if (duration.TotalMinutes >= 1)
{
return duration.ToString(@"m\:ss");
}
else
{
return $"{duration.TotalSeconds:F1}s";
}
}
protected async Task HideModal()
{
try
{
await JSRuntime.InvokeVoidAsync("eval",
"if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { " +
"var modalElement = document.getElementById('executionDetailModal'); " +
"var modal = bootstrap.Modal.getInstance(modalElement); " +
"if (modal) modal.hide(); " +
"} else { " +
"document.getElementById('executionDetailModal').style.display = 'none'; " +
"document.getElementById('executionDetailModal').classList.remove('show'); " +
"document.body.classList.remove('modal-open'); " +
"var backdrop = document.querySelector('.modal-backdrop'); " +
"if (backdrop) backdrop.remove(); " +
"}");
}
catch (Exception)
{
// Fallback: nascondi il modal manualmente
await JSRuntime.InvokeVoidAsync("eval",
"var modal = document.getElementById('executionDetailModal');" +
"modal.style.display = 'none';" +
"modal.classList.remove('show');" +
"document.body.classList.remove('modal-open');" +
"var backdrop = document.querySelector('.modal-backdrop');" +
"if (backdrop) backdrop.remove();");
}
}
private async Task ShowErrorMessage(string message)
{
await JSRuntime.InvokeVoidAsync("alert", "Errore: " + message);
}
}
+229
View File
@@ -0,0 +1,229 @@
@page "/settings"
@using Data_Coupler.Services
@using Data_Coupler.Models
@inject IJSRuntime JSRuntime
@inject ILogger<SettingsPage> Logger
<PageTitle>Impostazioni - Data Coupler</PageTitle>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>
<i class="fas fa-cog text-primary"></i>
Impostazioni Sistema
</h1>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item active" aria-current="page">Impostazioni</li>
</ol>
</nav>
</div>
@if (!string.IsNullOrEmpty(toastMessage))
{
<div class="alert alert-@(toastType == "success" ? "success" : toastType == "error" ? "danger" : "info") alert-dismissible fade show" role="alert">
<i class="fas fa-@(toastType == "success" ? "check-circle" : toastType == "error" ? "exclamation-circle" : "info-circle")"></i>
@toastMessage
<button type="button" class="btn-close" @onclick="ClearToast" aria-label="Close"></button>
</div>
}
<!-- Tab Navigation -->
<ul class="nav nav-tabs mb-4" id="settingsTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link @(activeTab == "backup" ? "active" : "")"
id="backup-tab"
@onclick='() => SetActiveTab("backup")'
type="button"
role="tab"
aria-controls="backup"
aria-selected="@(activeTab == "backup")">
<i class="fas fa-download me-2"></i>
Backup e Ripristino
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @(activeTab == "system" ? "active" : "")"
id="system-tab"
@onclick='() => SetActiveTab("system")'
type="button"
role="tab"
aria-controls="system"
aria-selected="@(activeTab == "system")">
<i class="fas fa-server me-2"></i>
Sistema
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @(activeTab == "security" ? "active" : "")"
id="security-tab"
@onclick='() => SetActiveTab("security")'
type="button"
role="tab"
aria-controls="security"
aria-selected="@(activeTab == "security")">
<i class="fas fa-shield-alt me-2"></i>
Sicurezza
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @(activeTab == "maintenance" ? "active" : "")"
id="maintenance-tab"
@onclick='() => SetActiveTab("maintenance")'
type="button"
role="tab"
aria-controls="maintenance"
aria-selected="@(activeTab == "maintenance")">
<i class="fas fa-tools me-2"></i>
Manutenzione
</button>
</li>
</ul>
<!-- Tab Content -->
<div class="tab-content" id="settingsTabContent">
<!-- Backup Tab -->
<div class="tab-pane fade @(activeTab == "backup" ? "show active" : "")"
id="backup"
role="tabpanel"
aria-labelledby="backup-tab">
<BackupTab OnShowToast="ShowToastTuple" />
</div>
<!-- System Tab -->
<div class="tab-pane fade @(activeTab == "system" ? "show active" : "")"
id="system"
role="tabpanel"
aria-labelledby="system-tab">
<SystemTab OnShowToast="ShowToastTuple" />
</div>
<!-- Security Tab -->
<div class="tab-pane fade @(activeTab == "security" ? "show active" : "")"
id="security"
role="tabpanel"
aria-labelledby="security-tab">
<SecurityTab OnShowToast="ShowToastTuple" />
</div>
<!-- Maintenance Tab -->
<div class="tab-pane fade @(activeTab == "maintenance" ? "show active" : "")"
id="maintenance"
role="tabpanel"
aria-labelledby="maintenance-tab">
<MaintenanceTab OnShowToast="ShowToastTuple" />
</div>
</div>
</div>
</div>
</div>
<!-- Include CSS per migliorare l'aspetto -->
<style>
.nav-tabs .nav-link {
border: 1px solid transparent;
border-radius: 0.375rem 0.375rem 0 0;
color: #6c757d;
background-color: #f8f9fa;
margin-right: 2px;
transition: all 0.15s ease-in-out;
}
.nav-tabs .nav-link:hover {
border-color: #e9ecef #e9ecef #dee2e6;
color: #495057;
background-color: #e9ecef;
}
.nav-tabs .nav-link.active {
color: #495057;
background-color: #fff;
border-color: #dee2e6 #dee2e6 #fff;
border-bottom: 1px solid #fff;
margin-bottom: -1px;
}
.tab-content {
border: 1px solid #dee2e6;
border-top: none;
border-radius: 0 0 0.375rem 0.375rem;
padding: 1.5rem;
background-color: #fff;
min-height: 500px;
}
.settings-section {
margin-bottom: 2rem;
}
.settings-section h4 {
border-bottom: 2px solid #e9ecef;
padding-bottom: 0.5rem;
margin-bottom: 1rem;
color: #495057;
}
.alert {
border-radius: 0.375rem;
border: none;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.card {
border: none;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
border-radius: 0.5rem;
}
.card-header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
border-radius: 0.5rem 0.5rem 0 0 !important;
}
</style>
@code {
private string activeTab = "backup";
private string toastMessage = "";
private string toastType = "info";
protected override async Task OnInitializedAsync()
{
// Inizializzazione se necessaria
await base.OnInitializedAsync();
}
private void SetActiveTab(string tabName)
{
activeTab = tabName;
StateHasChanged();
}
private void ShowToast(string message, string type = "info")
{
toastMessage = message;
toastType = type;
StateHasChanged();
// Auto-hide dopo 5 secondi per messaggi di successo
if (type == "success")
{
_ = Task.Delay(5000).ContinueWith(_ => ClearToast());
}
}
private void ShowToastTuple((string message, string type) toast)
{
ShowToast(toast.message, toast.type);
}
private void ClearToast()
{
toastMessage = "";
toastType = "info";
InvokeAsync(StateHasChanged);
}
}
+1
View File
@@ -31,6 +31,7 @@
</div>
<script src="_framework/blazor.server.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="js/site.js"></script>
<script>
window.downloadFileFromStream = async (fileName, contentStreamReference) => {
+112 -21
View File
@@ -8,42 +8,78 @@ using DataConnection.Enums;
using DataConnection.CredentialManagement;
using CredentialManager;
using Data_Coupler.Services;
using Data_Coupler.BackgroundServices;
using CredentialManager.Services;
using System;
using System.Threading.Tasks;
using Data_Coupler.BackgrounServices;
// Registra il provider di encoding per ExcelDataReader (necessario per file .xls)
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
var builder = WebApplication.CreateBuilder(args);
// Configurazione per Windows Service
builder.Host.UseWindowsService(options =>
{
options.ServiceName = "DataCouplerService";
});
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddWindowsService();
builder.Services.AddHostedService<BackgroundServices>();
// Configurazione logging per Windows Service
if (OperatingSystem.IsWindows())
{
builder.Logging.AddEventLog();
}
#region Database Directory Path management
string dbPath = string.Empty;
if (OperatingSystem.IsWindows())
try
{
dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Data_Coupler", "credentials.db");
}
else if (OperatingSystem.IsLinux())
{
dbPath = "/var/lib/Data_Coupler/credentials.db";
}
else if (OperatingSystem.IsMacOS())
{
dbPath = "/Library/Application Support/Data_Coupler/credentials.db";
}
if (OperatingSystem.IsWindows())
{
// Per servizi Windows, usa una cartella con permessi appropriati
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
if (string.IsNullOrEmpty(appDataPath))
{
// Fallback per servizi Windows
appDataPath = @"C:\ProgramData";
}
dbPath = Path.Combine(appDataPath, "Data_Coupler", "credentials.db");
}
else if (OperatingSystem.IsLinux())
{
dbPath = "/var/lib/Data_Coupler/credentials.db";
}
else if (OperatingSystem.IsMacOS())
{
dbPath = "/Library/Application Support/Data_Coupler/credentials.db";
}
var dbDirectory = Path.GetDirectoryName(dbPath);
if (!Directory.Exists(dbDirectory))
var dbDirectory = Path.GetDirectoryName(dbPath);
if (!Directory.Exists(dbDirectory))
{
Directory.CreateDirectory(dbDirectory!);
// Per Windows, assicurati che la cartella abbia i permessi corretti
if (OperatingSystem.IsWindows() && !string.IsNullOrEmpty(dbDirectory))
{
var directoryInfo = new DirectoryInfo(dbDirectory);
// Imposta permessi per consentire l'accesso al servizio
}
}
Console.WriteLine($"Database path: {dbPath}");
}
catch (Exception ex)
{
Directory.CreateDirectory(dbDirectory!);
Console.WriteLine($"Errore nella configurazione del percorso database: {ex.Message}");
throw;
}
#endregion
@@ -63,24 +99,79 @@ builder.Services.AddHttpClient();
// Register Data Connection Factory
builder.Services.AddScoped<IDataConnectionFactory, DataConnectionFactory>();
builder.WebHost.UseUrls("http://*:7550");
// Register Backup Service
builder.Services.AddScoped<Data_Coupler.Services.IBackupService, Data_Coupler.Services.BackupService>();
// Register Schedule Services
builder.Services.AddScoped<IProfileScheduleService, ProfileScheduleService>();
// Register Data Transfer Service
builder.Services.AddScoped<Data_Coupler.Services.IDataTransferService, Data_Coupler.Services.DataTransferService>();
// Register Scheduled Profile Execution Service
builder.Services.AddScoped<Data_Coupler.Services.IScheduledProfileExecutionService, Data_Coupler.Services.ScheduledProfileExecutionService>();
// Register Background Services (solo uno per evitare duplicazioni)
builder.Services.AddHostedService<Data_Coupler.BackgroundServices.ScheduledJobService>();
// Configurazione URL e timeout per servizio Windows
var urls = builder.Configuration.GetValue<string>("Urls") ?? "http://*:7550";
builder.WebHost.UseUrls(urls);
// Configurazione timeout per servizio Windows
builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(10);
serverOptions.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(5);
});
var app = builder.Build();
// Initialize database
// Initialize database con timeout e retry
using (var scope = app.Services.CreateScope())
{
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
try
{
logger.LogInformation("Inizializzazione database in corso...");
logger.LogInformation("Inizializzazione database in corso... Path: {DbPath}", dbPath);
var dbInitializer = scope.ServiceProvider.GetRequiredService<CredentialManager.Services.IDatabaseInitializer>();
dbInitializer.InitializeAsync().GetAwaiter().GetResult();
// Inizializzazione con timeout di 60 secondi
var initTask = dbInitializer.InitializeAsync();
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(60));
var completedTask = await Task.WhenAny(initTask, timeoutTask);
if (completedTask == timeoutTask)
{
logger.LogError("Timeout durante l'inizializzazione del database (60 secondi)");
throw new TimeoutException("Timeout durante l'inizializzazione del database");
}
await initTask; // Attendi il completamento per eventuali eccezioni
logger.LogInformation("Database inizializzato con successo.");
}
catch (Exception ex)
{
logger.LogError(ex, "Errore durante l'inizializzazione del database: {Message}", ex.Message);
logger.LogError(ex, "Errore durante l'inizializzazione del database: {Message}. Path: {DbPath}", ex.Message, dbPath);
// Per servizi Windows, log su Event Log
if (OperatingSystem.IsWindows())
{
try
{
using var eventLog = new System.Diagnostics.EventLog("Application");
eventLog.Source = "DataCouplerService";
eventLog.WriteEntry($"Errore inizializzazione database: {ex.Message}", System.Diagnostics.EventLogEntryType.Error);
}
catch
{
// Ignora errori di scrittura EventLog
}
}
throw; // Rilancia l'eccezione per non far partire l'app con un database non funzionante
}
}
@@ -0,0 +1,44 @@
-- Migrazione per aggiungere supporto completo allo scheduling con storico esecuzioni
-- 1. Aggiungi colonne per override database nella tabella ProfileSchedules
ALTER TABLE ProfileSchedules ADD COLUMN SourceDatabaseOverride TEXT;
ALTER TABLE ProfileSchedules ADD COLUMN DestinationDatabaseOverride TEXT;
-- 2. Crea tabella per lo storico delle esecuzioni
CREATE TABLE IF NOT EXISTS ScheduleExecutionHistories (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
ScheduleId INTEGER NOT NULL,
ProfileId INTEGER NOT NULL,
ProfileName TEXT NOT NULL,
StartTime DATETIME NOT NULL,
EndTime DATETIME,
Status TEXT NOT NULL,
Message TEXT,
RecordsProcessed INTEGER DEFAULT 0,
RecordsWithErrors INTEGER,
ErrorDetails TEXT,
TriggerType TEXT NOT NULL,
TriggeredBy TEXT,
SourceType TEXT,
DestinationType TEXT,
SourceInfo TEXT,
DestinationInfo TEXT,
AdditionalInfo TEXT,
CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (ScheduleId) REFERENCES ProfileSchedules (Id) ON DELETE CASCADE
);
-- 3. Crea indici per ottimizzare le query
CREATE INDEX IF NOT EXISTS IDX_ScheduleExecutionHistories_ScheduleId ON ScheduleExecutionHistories (ScheduleId);
CREATE INDEX IF NOT EXISTS IDX_ScheduleExecutionHistories_ProfileId ON ScheduleExecutionHistories (ProfileId);
CREATE INDEX IF NOT EXISTS IDX_ScheduleExecutionHistories_Status ON ScheduleExecutionHistories (Status);
CREATE INDEX IF NOT EXISTS IDX_ScheduleExecutionHistories_StartTime ON ScheduleExecutionHistories (StartTime);
CREATE INDEX IF NOT EXISTS IDX_ScheduleExecutionHistories_TriggerType ON ScheduleExecutionHistories (TriggerType);
-- 4. Aggiorna eventuali schedulazioni esistenti per impostare NextExecutionTime se null
-- Questo è utile se ci sono già delle schedulazioni nel database
UPDATE ProfileSchedules
SET NextExecutionTime = datetime('now', '+1 hour')
WHERE NextExecutionTime IS NULL AND IsActive = 1 AND IsEnabled = 1;
PRAGMA foreign_keys = ON;
+733
View File
@@ -0,0 +1,733 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using CredentialManager.Data;
using CredentialManager.Models;
using CredentialManager.Services;
using Data_Coupler.Models;
namespace Data_Coupler.Services;
/// <summary>
/// Interfaccia per il servizio di backup e ripristino
/// </summary>
public interface IBackupService
{
/// <summary>
/// Esporta tutti i dati del sistema in un file di backup
/// </summary>
Task<BackupOperationResult> ExportBackupAsync(BackupOptions options);
/// <summary>
/// Importa i dati da un file di backup
/// </summary>
Task<BackupOperationResult> ImportBackupAsync(string backupFilePath, RestoreOptions options);
/// <summary>
/// Importa i dati da contenuto JSON
/// </summary>
Task<BackupOperationResult> ImportBackupFromJsonAsync(string jsonContent, RestoreOptions options);
/// <summary>
/// Valida un file di backup
/// </summary>
Task<BackupOperationResult> ValidateBackupAsync(string backupFilePath);
/// <summary>
/// Ottiene le informazioni su un backup senza importarlo
/// </summary>
Task<SystemBackupData?> GetBackupInfoAsync(string backupFilePath);
}
/// <summary>
/// Servizio per la gestione dei backup e ripristini del sistema
/// </summary>
public class BackupService : IBackupService
{
private readonly CredentialDbContext _context;
private readonly IDataCouplerProfileService _profileService;
private readonly ICredentialService _credentialService;
private readonly IKeyAssociationService _keyAssociationService;
private readonly ILogger<BackupService> _logger;
public BackupService(
CredentialDbContext context,
IDataCouplerProfileService profileService,
ICredentialService credentialService,
IKeyAssociationService keyAssociationService,
ILogger<BackupService> logger)
{
_context = context;
_profileService = profileService;
_credentialService = credentialService;
_keyAssociationService = keyAssociationService;
_logger = logger;
}
/// <summary>
/// Esporta tutti i dati del sistema
/// </summary>
public async Task<BackupOperationResult> ExportBackupAsync(BackupOptions options)
{
var result = new BackupOperationResult();
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
_logger.LogInformation("Avvio backup con opzioni: {@Options}", options);
var backupData = new SystemBackupData
{
Metadata = new BackupMetadata
{
CreatedAt = DateTime.UtcNow,
CreatedBy = options.CreatedBy,
Description = options.Description,
ApplicationVersion = "1.0.0"
}
};
// Export Profiles
if (options.IncludeProfiles)
{
_logger.LogDebug("Esportazione profili in corso...");
backupData.Profiles = await ExportProfilesAsync(options.IncludeOnlyActiveRecords);
result.ProcessedCounts.Profiles = backupData.Profiles.Count;
_logger.LogDebug("Esportati {Count} profili", backupData.Profiles.Count);
}
// Export Credentials (senza dati sensibili)
if (options.IncludeCredentials)
{
_logger.LogDebug("Esportazione credenziali in corso...");
backupData.Credentials = await ExportCredentialsAsync(options.IncludeOnlyActiveRecords);
result.ProcessedCounts.Credentials = backupData.Credentials.Count;
result.Warnings.Add("Le credenziali esportate non includono password, API keys o token per motivi di sicurezza");
_logger.LogDebug("Esportate {Count} credenziali", backupData.Credentials.Count);
}
// Export Key Associations
if (options.IncludeKeyAssociations)
{
_logger.LogDebug("Esportazione associazioni chiavi in corso...");
backupData.KeyAssociations = await ExportKeyAssociationsAsync(options.IncludeOnlyActiveRecords);
result.ProcessedCounts.KeyAssociations = backupData.KeyAssociations.Count;
_logger.LogDebug("Esportate {Count} associazioni", backupData.KeyAssociations.Count);
}
// Export Profile Schedules
if (options.IncludeProfileSchedules)
{
_logger.LogDebug("Esportazione schedule profili in corso...");
backupData.ProfileSchedules = await ExportProfileSchedulesAsync(options.IncludeOnlyActiveRecords);
result.ProcessedCounts.ProfileSchedules = backupData.ProfileSchedules.Count;
_logger.LogDebug("Esportate {Count} schedule", backupData.ProfileSchedules.Count);
}
// Aggiorna metadata con conteggi
backupData.Metadata.RecordCounts = result.ProcessedCounts;
// Serializza e salva
var jsonOptions = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var json = JsonSerializer.Serialize(backupData, jsonOptions);
var fileName = $"data_coupler_backup_{DateTime.Now:yyyyMMdd_HHmmss}.json";
var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
var backupFolder = Path.Combine(documentsPath, "DataCoupler", "Backups");
Directory.CreateDirectory(backupFolder);
var filePath = Path.Combine(backupFolder, fileName);
await File.WriteAllTextAsync(filePath, json);
result.FilePath = filePath;
result.Success = true;
result.Message = $"Backup completato con successo. File salvato: {filePath}";
_logger.LogInformation("Backup completato: {FilePath}, Record totali: {Total}",
filePath,
result.ProcessedCounts.Profiles + result.ProcessedCounts.Credentials +
result.ProcessedCounts.KeyAssociations + result.ProcessedCounts.ProfileSchedules);
}
catch (Exception ex)
{
result.Success = false;
result.Message = $"Errore durante il backup: {ex.Message}";
result.Errors.Add(ex.ToString());
_logger.LogError(ex, "Errore durante l'esportazione del backup");
}
finally
{
stopwatch.Stop();
result.Duration = stopwatch.Elapsed;
}
return result;
}
/// <summary>
/// Importa dati da file backup
/// </summary>
public async Task<BackupOperationResult> ImportBackupAsync(string backupFilePath, RestoreOptions options)
{
if (!File.Exists(backupFilePath))
{
return new BackupOperationResult
{
Success = false,
Message = "File di backup non trovato"
};
}
var json = await File.ReadAllTextAsync(backupFilePath);
return await ImportBackupFromJsonAsync(json, options);
}
/// <summary>
/// Importa dati da contenuto JSON
/// </summary>
public async Task<BackupOperationResult> ImportBackupFromJsonAsync(string jsonContent, RestoreOptions options)
{
var result = new BackupOperationResult();
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
_logger.LogInformation("Avvio import backup con opzioni: {@Options}", options);
// Parse JSON
var jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var backupData = JsonSerializer.Deserialize<SystemBackupData>(jsonContent, jsonOptions);
if (backupData == null)
{
throw new InvalidOperationException("Impossibile deserializzare i dati del backup");
}
_logger.LogInformation("Backup da importare: {Metadata}", backupData.Metadata);
// Crea backup automatico prima del restore se richiesto
if (options.CreateBackupBeforeRestore)
{
_logger.LogInformation("Creazione backup di sicurezza pre-restore...");
var preRestoreBackup = await ExportBackupAsync(new BackupOptions
{
Description = "Backup automatico pre-restore",
CreatedBy = options.ImportedBy
});
if (preRestoreBackup.Success)
{
result.Warnings.Add($"Backup di sicurezza creato: {preRestoreBackup.FilePath}");
}
}
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// Import Profiles
if (options.RestoreProfiles && backupData.Profiles.Any())
{
_logger.LogDebug("Importazione profili in corso...");
var importedProfiles = await ImportProfilesAsync(backupData.Profiles, options);
result.ProcessedCounts.Profiles = importedProfiles;
_logger.LogDebug("Importati {Count} profili", importedProfiles);
}
// Import Credentials (solo metadati, nessun dato sensibile)
if (options.RestoreCredentials && backupData.Credentials.Any())
{
_logger.LogDebug("Importazione credenziali in corso...");
var importedCredentials = await ImportCredentialsAsync(backupData.Credentials, options);
result.ProcessedCounts.Credentials = importedCredentials;
result.Warnings.Add("Le credenziali importate richiedono configurazione manuale di password/API keys");
_logger.LogDebug("Importate {Count} credenziali", importedCredentials);
}
// Import Key Associations
if (options.RestoreKeyAssociations && backupData.KeyAssociations.Any())
{
_logger.LogDebug("Importazione associazioni chiavi in corso...");
var importedAssociations = await ImportKeyAssociationsAsync(backupData.KeyAssociations, options);
result.ProcessedCounts.KeyAssociations = importedAssociations;
_logger.LogDebug("Importate {Count} associazioni", importedAssociations);
}
// Import Profile Schedules
if (options.RestoreProfileSchedules && backupData.ProfileSchedules.Any())
{
_logger.LogDebug("Importazione schedule profili in corso...");
var importedSchedules = await ImportProfileSchedulesAsync(backupData.ProfileSchedules, options);
result.ProcessedCounts.ProfileSchedules = importedSchedules;
_logger.LogDebug("Importate {Count} schedule", importedSchedules);
}
await transaction.CommitAsync();
result.Success = true;
result.Message = "Import completato con successo";
_logger.LogInformation("Import completato con successo. Record totali: {Total}",
result.ProcessedCounts.Profiles + result.ProcessedCounts.Credentials +
result.ProcessedCounts.KeyAssociations + result.ProcessedCounts.ProfileSchedules);
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
catch (Exception ex)
{
result.Success = false;
result.Message = $"Errore durante l'import: {ex.Message}";
result.Errors.Add(ex.ToString());
_logger.LogError(ex, "Errore durante l'importazione del backup");
}
finally
{
stopwatch.Stop();
result.Duration = stopwatch.Elapsed;
}
return result;
}
/// <summary>
/// Valida un file di backup
/// </summary>
public async Task<BackupOperationResult> ValidateBackupAsync(string backupFilePath)
{
var result = new BackupOperationResult();
try
{
if (!File.Exists(backupFilePath))
{
result.Errors.Add("File non trovato");
return result;
}
var json = await File.ReadAllTextAsync(backupFilePath);
var backupData = JsonSerializer.Deserialize<SystemBackupData>(json);
if (backupData == null)
{
result.Errors.Add("Formato backup non valido");
return result;
}
// Validazioni
if (backupData.Metadata == null)
{
result.Errors.Add("Metadata mancanti");
}
if (string.IsNullOrEmpty(backupData.Metadata?.Version))
{
result.Warnings.Add("Versione backup non specificata");
}
result.ProcessedCounts.Profiles = backupData.Profiles?.Count ?? 0;
result.ProcessedCounts.Credentials = backupData.Credentials?.Count ?? 0;
result.ProcessedCounts.KeyAssociations = backupData.KeyAssociations?.Count ?? 0;
result.ProcessedCounts.ProfileSchedules = backupData.ProfileSchedules?.Count ?? 0;
result.Success = result.Errors.Count == 0;
result.Message = result.Success ? "Backup valido" : "Backup contiene errori";
}
catch (Exception ex)
{
result.Success = false;
result.Message = $"Errore validazione: {ex.Message}";
result.Errors.Add(ex.ToString());
}
return result;
}
/// <summary>
/// Ottiene informazioni backup senza importare
/// </summary>
public async Task<SystemBackupData?> GetBackupInfoAsync(string backupFilePath)
{
try
{
if (!File.Exists(backupFilePath))
return null;
var json = await File.ReadAllTextAsync(backupFilePath);
return JsonSerializer.Deserialize<SystemBackupData>(json);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Errore lettura info backup da {FilePath}", backupFilePath);
return null;
}
}
#region Private Export Methods
private async Task<List<DataCouplerProfileBackup>> ExportProfilesAsync(bool onlyActive)
{
var query = _context.DataCouplerProfiles
.Include(p => p.SourceCredential)
.Include(p => p.DestinationCredential)
.AsQueryable();
if (onlyActive)
query = query.Where(p => p.IsActive);
var profiles = await query.ToListAsync();
return profiles.Select(p => new DataCouplerProfileBackup
{
Id = p.Id,
Name = p.Name,
Description = p.Description,
SourceType = p.SourceType,
SourceCredentialName = p.SourceCredential?.Name,
SourceDatabaseName = p.SourceDatabaseName,
SourceSchema = p.SourceSchema,
SourceTable = p.SourceTable,
SourceCustomQuery = p.SourceCustomQuery,
SourceFilePath = p.SourceFilePath,
DestinationType = p.DestinationType,
DestinationCredentialName = p.DestinationCredential?.Name,
DestinationSchema = p.DestinationSchema,
DestinationTable = p.DestinationTable,
DestinationEndpoint = p.DestinationEndpoint,
FieldMappings = !string.IsNullOrEmpty(p.FieldMappingJson) ?
System.Text.Json.JsonSerializer.Deserialize<List<FieldMappingDto>>(p.FieldMappingJson) ?? new List<FieldMappingDto>() :
new List<FieldMappingDto>(),
SourceKeyField = p.SourceKeyField,
UseRecordAssociations = p.UseRecordAssociations,
CreatedBy = p.CreatedBy,
CreatedAt = p.CreatedAt,
LastUsedAt = p.LastUsedAt,
IsActive = p.IsActive
}).ToList();
}
private async Task<List<CredentialBackup>> ExportCredentialsAsync(bool onlyActive)
{
var query = _context.Credentials.AsQueryable();
if (onlyActive)
query = query.Where(c => c.IsActive);
var credentials = await query.ToListAsync();
return credentials.Select(c => new CredentialBackup
{
Id = c.Id,
Name = c.Name,
Type = c.Type,
DatabaseType = c.DatabaseType,
Host = c.Host,
Port = c.Port,
DatabaseName = c.DatabaseName,
Username = c.Username,
// Password, API Keys e Token NON inclusi per sicurezza
CommandTimeout = c.CommandTimeout,
TimeoutSeconds = c.TimeoutSeconds,
IgnoreSslErrors = c.IgnoreSslErrors,
RestServiceType = c.RestServiceType,
Headers = c.Headers,
AdditionalParameters = c.AdditionalParameters,
CreatedAt = c.CreatedAt,
UpdatedAt = c.UpdatedAt,
CreatedBy = c.CreatedBy,
IsActive = c.IsActive
}).ToList();
}
private async Task<List<KeyAssociationBackup>> ExportKeyAssociationsAsync(bool onlyActive)
{
var query = _context.KeyAssociations.AsQueryable();
if (onlyActive)
query = query.Where(ka => ka.IsActive);
var associations = await query.ToListAsync();
return associations.Select(ka => new KeyAssociationBackup
{
Id = ka.Id,
KeyValue = ka.KeyValue,
SourceKeyField = ka.SourceKeyField,
DestinationKeyField = ka.DestinationKeyField,
DestinationEntity = ka.DestinationEntity,
DestinationId = ka.DestinationId,
RestCredentialName = ka.RestCredentialName,
CreatedAt = ka.CreatedAt,
UpdatedAt = ka.UpdatedAt,
LastVerifiedAt = ka.LastVerifiedAt,
IsActive = ka.IsActive,
DataHash = ka.Data_Hash,
SourcesInfo = ka.SourcesInfo,
AdditionalInfo = ka.AdditionalInfo
}).ToList();
}
private Task<List<ProfileScheduleBackup>> ExportProfileSchedulesAsync(bool onlyActive)
{
// Nota: Assumendo che esista una tabella ProfileSchedules
// Se non esiste, questo metodo restituirà una lista vuota
var schedules = new List<ProfileScheduleBackup>();
try
{
// TODO: Implementare quando la tabella ProfileSchedules sarà disponibile
// var query = _context.ProfileSchedules.AsQueryable();
// if (onlyActive)
// query = query.Where(ps => ps.IsActive);
// var profileSchedules = await query.ToListAsync();
// schedules = profileSchedules.Select(ps => new ProfileScheduleBackup { ... }).ToList();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Tabella ProfileSchedules non disponibile, saltando export");
}
return Task.FromResult(schedules);
}
#endregion
#region Private Import Methods
private async Task<int> ImportProfilesAsync(List<DataCouplerProfileBackup> profiles, RestoreOptions options)
{
int imported = 0;
foreach (var profileBackup in profiles)
{
try
{
var existing = await _context.DataCouplerProfiles
.FirstOrDefaultAsync(p => p.Name == profileBackup.Name);
if (existing != null && !options.OverwriteExisting)
{
_logger.LogDebug("Profilo {Name} già esistente, saltando", profileBackup.Name);
continue;
}
var profile = new DataCouplerProfile
{
Name = profileBackup.Name,
Description = profileBackup.Description,
SourceType = profileBackup.SourceType,
SourceDatabaseName = profileBackup.SourceDatabaseName,
SourceSchema = profileBackup.SourceSchema,
SourceTable = profileBackup.SourceTable,
SourceCustomQuery = profileBackup.SourceCustomQuery,
SourceFilePath = profileBackup.SourceFilePath,
DestinationType = profileBackup.DestinationType,
DestinationSchema = profileBackup.DestinationSchema,
DestinationTable = profileBackup.DestinationTable,
DestinationEndpoint = profileBackup.DestinationEndpoint,
FieldMappingJson = profileBackup.FieldMappings != null ?
System.Text.Json.JsonSerializer.Serialize(profileBackup.FieldMappings) :
string.Empty,
SourceKeyField = profileBackup.SourceKeyField,
UseRecordAssociations = profileBackup.UseRecordAssociations,
CreatedBy = options.ImportedBy ?? profileBackup.CreatedBy,
CreatedAt = DateTime.UtcNow,
IsActive = profileBackup.IsActive
};
// Risolvi credential IDs per nome se esistenti
if (!string.IsNullOrEmpty(profileBackup.SourceCredentialName))
{
var sourceCred = await _context.Credentials
.FirstOrDefaultAsync(c => c.Name == profileBackup.SourceCredentialName);
profile.SourceCredentialId = sourceCred?.Id;
}
if (!string.IsNullOrEmpty(profileBackup.DestinationCredentialName))
{
var destCred = await _context.Credentials
.FirstOrDefaultAsync(c => c.Name == profileBackup.DestinationCredentialName);
profile.DestinationCredentialId = destCred?.Id;
}
if (existing != null)
{
// Update existing
_context.Entry(existing).CurrentValues.SetValues(profile);
existing.Id = profileBackup.Id; // Mantieni ID originale se sovrascriviamo
}
else
{
// Add new
_context.DataCouplerProfiles.Add(profile);
}
imported++;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Errore importazione profilo {Name}", profileBackup.Name);
}
}
await _context.SaveChangesAsync();
return imported;
}
private async Task<int> ImportCredentialsAsync(List<CredentialBackup> credentials, RestoreOptions options)
{
int imported = 0;
foreach (var credBackup in credentials)
{
try
{
var existing = await _context.Credentials
.FirstOrDefaultAsync(c => c.Name == credBackup.Name);
if (existing != null && !options.OverwriteExisting)
{
_logger.LogDebug("Credenziale {Name} già esistente, saltando", credBackup.Name);
continue;
}
var credential = new CredentialEntity
{
Name = credBackup.Name,
Type = credBackup.Type,
DatabaseType = credBackup.DatabaseType,
Host = credBackup.Host,
Port = credBackup.Port,
DatabaseName = credBackup.DatabaseName,
Username = credBackup.Username,
// Password, API Keys e Token dovranno essere riconfigurati manualmente
CommandTimeout = credBackup.CommandTimeout,
TimeoutSeconds = credBackup.TimeoutSeconds,
IgnoreSslErrors = credBackup.IgnoreSslErrors,
RestServiceType = credBackup.RestServiceType,
Headers = credBackup.Headers,
AdditionalParameters = credBackup.AdditionalParameters,
CreatedAt = DateTime.UtcNow,
CreatedBy = options.ImportedBy ?? credBackup.CreatedBy,
IsActive = credBackup.IsActive
};
if (existing != null)
{
// Update existing (preserva password esistenti)
var oldPassword = existing.EncryptedPassword;
var oldApiKey = existing.EncryptedApiKey;
var oldAuthToken = existing.EncryptedAuthToken;
_context.Entry(existing).CurrentValues.SetValues(credential);
existing.Id = credBackup.Id;
existing.EncryptedPassword = oldPassword;
existing.EncryptedApiKey = oldApiKey;
existing.EncryptedAuthToken = oldAuthToken;
}
else
{
// Add new
_context.Credentials.Add(credential);
}
imported++;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Errore importazione credenziale {Name}", credBackup.Name);
}
}
await _context.SaveChangesAsync();
return imported;
}
private async Task<int> ImportKeyAssociationsAsync(List<KeyAssociationBackup> associations, RestoreOptions options)
{
int imported = 0;
foreach (var assocBackup in associations)
{
try
{
var existing = await _context.KeyAssociations
.FirstOrDefaultAsync(ka => ka.KeyValue == assocBackup.KeyValue &&
ka.DestinationEntity == assocBackup.DestinationEntity &&
ka.RestCredentialName == assocBackup.RestCredentialName);
if (existing != null && !options.OverwriteExisting)
{
_logger.LogDebug("Associazione {Key}-{Entity} già esistente, saltando",
assocBackup.KeyValue, assocBackup.DestinationEntity);
continue;
}
var association = new KeyAssociation
{
KeyValue = assocBackup.KeyValue,
SourceKeyField = assocBackup.SourceKeyField,
DestinationKeyField = assocBackup.DestinationKeyField,
DestinationEntity = assocBackup.DestinationEntity,
DestinationId = assocBackup.DestinationId,
RestCredentialName = assocBackup.RestCredentialName,
CreatedAt = DateTime.UtcNow,
IsActive = assocBackup.IsActive,
Data_Hash = assocBackup.DataHash,
SourcesInfo = assocBackup.SourcesInfo,
AdditionalInfo = assocBackup.AdditionalInfo
};
if (existing != null)
{
// Update existing
_context.Entry(existing).CurrentValues.SetValues(association);
existing.Id = assocBackup.Id;
}
else
{
// Add new
_context.KeyAssociations.Add(association);
}
imported++;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Errore importazione associazione {Key}-{Entity}",
assocBackup.KeyValue, assocBackup.DestinationEntity);
}
}
await _context.SaveChangesAsync();
return imported;
}
private Task<int> ImportProfileSchedulesAsync(List<ProfileScheduleBackup> schedules, RestoreOptions options)
{
int imported = 0;
try
{
// TODO: Implementare quando la tabella ProfileSchedules sarà disponibile
_logger.LogInformation("Import ProfileSchedules saltato - tabella non ancora implementata");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Errore importazione schedule profili");
}
return Task.FromResult(imported);
}
#endregion
}
@@ -0,0 +1,210 @@
using CredentialManager.Models;
using Microsoft.Extensions.Logging;
namespace Data_Coupler.Services;
/// <summary>
/// Servizio per l'esecuzione automatica dei trasferimenti dati basati sui profili
/// </summary>
public interface IDataTransferService
{
Task<DataTransferResult> ExecuteProfileAsync(DataCouplerProfile profile, string? sourceDatabaseOverride = null, string? destinationDatabaseOverride = null);
}
public class DataTransferResult
{
public bool IsSuccess { get; set; }
public int RecordsProcessed { get; set; }
public string? ErrorMessage { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public List<string> ErrorDetails { get; set; } = new();
public Dictionary<string, object> AdditionalInfo { get; set; } = new();
public TimeSpan Duration => EndTime - StartTime;
}
public class DataTransferService : IDataTransferService
{
private readonly IScheduledProfileExecutionService _scheduledExecutionService;
private readonly ILogger<DataTransferService> _logger;
public DataTransferService(
IScheduledProfileExecutionService scheduledExecutionService,
ILogger<DataTransferService> logger)
{
_scheduledExecutionService = scheduledExecutionService ?? throw new ArgumentNullException(nameof(scheduledExecutionService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<DataTransferResult> ExecuteProfileAsync(DataCouplerProfile profile, string? sourceDatabaseOverride = null, string? destinationDatabaseOverride = null)
{
var result = new DataTransferResult
{
StartTime = DateTime.Now // Usa l'ora locale per coerenza con le schedulazioni
};
try
{
_logger.LogInformation("Iniziando esecuzione profilo {ProfileName} (ID: {ProfileId})",
profile.Name, profile.Id);
// Validazione del profilo
var validationResult = ValidateProfile(profile);
if (!validationResult.IsValid)
{
result.IsSuccess = false;
result.ErrorMessage = validationResult.ErrorMessage;
result.EndTime = DateTime.Now; // Usa l'ora locale per coerenza
return result;
}
// Controlla se il profilo ha file come sorgente e blocca l'esecuzione
if (profile.SourceType?.ToLower() == "file")
{
result.IsSuccess = false;
result.ErrorMessage = "I profili con file come sorgente non sono supportati nelle schedulazioni per motivi di sicurezza.";
result.EndTime = DateTime.Now; // Usa l'ora locale per coerenza
_logger.LogWarning("Tentativo di esecuzione di profilo con file come sorgente bloccato: {ProfileName}", profile.Name);
return result;
}
// Applica override del database se specificati
var profileToExecute = await ApplyDatabaseOverrides(profile, sourceDatabaseOverride, destinationDatabaseOverride);
// Utilizza il servizio esistente per l'esecuzione
var executionResult = await _scheduledExecutionService.ExecuteProfileAsync(profileToExecute.Id);
result.IsSuccess = executionResult.Success;
result.RecordsProcessed = executionResult.RecordsProcessed;
result.ErrorMessage = executionResult.Success ? null : executionResult.Message;
result.EndTime = DateTime.Now; // Usa l'ora locale per coerenza
if (executionResult.Success)
{
result.AdditionalInfo["ExecutionDuration"] = executionResult.Duration.ToString();
_logger.LogInformation("Profilo {ProfileName} eseguito con successo. " +
"Record processati: {RecordsProcessed}, Durata: {Duration}ms",
profile.Name, result.RecordsProcessed, result.Duration.TotalMilliseconds);
}
else
{
result.ErrorDetails.Add(executionResult.Message);
_logger.LogError("Errore nell'esecuzione del profilo {ProfileName}: {ErrorMessage}",
profile.Name, executionResult.Message);
}
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore durante l'esecuzione del profilo {ProfileName} (ID: {ProfileId})",
profile.Name, profile.Id);
result.IsSuccess = false;
result.ErrorMessage = ex.Message;
result.ErrorDetails.Add($"Exception: {ex.GetType().Name} - {ex.Message}");
if (ex.InnerException != null)
{
result.ErrorDetails.Add($"Inner Exception: {ex.InnerException.Message}");
}
result.EndTime = DateTime.Now; // Usa l'ora locale per coerenza
return result;
}
}
private async Task<DataCouplerProfile> ApplyDatabaseOverrides(DataCouplerProfile originalProfile,
string? sourceDatabaseOverride, string? destinationDatabaseOverride)
{
// Se non ci sono override, restituisce il profilo originale
if (string.IsNullOrEmpty(sourceDatabaseOverride) && string.IsNullOrEmpty(destinationDatabaseOverride))
{
return originalProfile;
}
// Crea una copia del profilo con gli override applicati
// In un'implementazione reale, potresti voler creare una copia più sofisticata
// o utilizzare un metodo di clonazione appropriato
var profileCopy = new DataCouplerProfile
{
Id = originalProfile.Id,
Name = originalProfile.Name,
Description = originalProfile.Description,
SourceType = originalProfile.SourceType,
SourceCredentialId = originalProfile.SourceCredentialId,
SourceDatabaseName = originalProfile.SourceDatabaseName,
SourceTable = originalProfile.SourceTable,
SourceSchema = originalProfile.SourceSchema,
SourceCustomQuery = originalProfile.SourceCustomQuery,
SourceFilePath = originalProfile.SourceFilePath,
DestinationType = originalProfile.DestinationType,
DestinationCredentialId = originalProfile.DestinationCredentialId,
DestinationTable = originalProfile.DestinationTable,
DestinationSchema = originalProfile.DestinationSchema,
DestinationEndpoint = originalProfile.DestinationEndpoint,
FieldMappingJson = originalProfile.FieldMappingJson,
SourceKeyField = originalProfile.SourceKeyField,
UseRecordAssociations = originalProfile.UseRecordAssociations,
IsActive = originalProfile.IsActive,
CreatedAt = originalProfile.CreatedAt,
LastUsedAt = originalProfile.LastUsedAt,
CreatedBy = originalProfile.CreatedBy
};
// TODO: Implementare l'applicazione degli override del database
// Questo richiederebbe di modificare temporaneamente la stringa di connessione
// delle credenziali per puntare al database specificato
_logger.LogInformation("Applicazione override database - Source: {SourceDB}, Destination: {DestDB}",
sourceDatabaseOverride ?? "none", destinationDatabaseOverride ?? "none");
return await Task.FromResult(profileCopy);
}
private (bool IsValid, string? ErrorMessage) ValidateProfile(DataCouplerProfile profile)
{
if (profile == null)
return (false, "Profilo non specificato");
if (string.IsNullOrEmpty(profile.Name))
return (false, "Nome profilo non specificato");
if (string.IsNullOrEmpty(profile.SourceType))
return (false, "Tipo sorgente non specificato");
if (string.IsNullOrEmpty(profile.DestinationType))
return (false, "Tipo destinazione non specificato");
if (!profile.SourceCredentialId.HasValue)
return (false, "Credenziale sorgente non specificata");
if (!profile.DestinationCredentialId.HasValue)
return (false, "Credenziale destinazione non specificata");
// Validazioni specifiche per tipo sorgente
if (profile.SourceType == "database")
{
if (string.IsNullOrEmpty(profile.SourceTable) && string.IsNullOrEmpty(profile.SourceCustomQuery))
return (false, "Tabella sorgente o query personalizzata deve essere specificata");
}
else if (profile.SourceType == "file")
{
if (string.IsNullOrEmpty(profile.SourceFilePath))
return (false, "Percorso file sorgente non specificato");
}
// Validazioni specifiche per tipo destinazione
if (profile.DestinationType == "database")
{
if (string.IsNullOrEmpty(profile.DestinationTable))
return (false, "Tabella destinazione non specificata");
}
else if (profile.DestinationType == "rest")
{
if (string.IsNullOrEmpty(profile.DestinationEndpoint))
return (false, "Endpoint REST destinazione non specificato");
}
return (true, null);
}
}
+195
View File
@@ -0,0 +1,195 @@
using System.Globalization;
namespace Data_Coupler.Services;
/// <summary>
/// Servizio utility per la gestione di date, orari e formattazione
/// Utilizza il formato 24h per coerenza con il sistema
/// </summary>
public static class DateTimeHelper
{
/// <summary>
/// Formato orario 24h standard utilizzato in tutto il sistema
/// </summary>
public const string TimeFormat24H = "HH:mm";
/// <summary>
/// Formato data/ora 24h completo utilizzato per il logging e la visualizzazione
/// </summary>
public const string DateTimeFormat24H = "dd/MM/yyyy HH:mm:ss";
/// <summary>
/// Formato data/ora 24h per i log dettagliati
/// </summary>
public const string DetailedDateTimeFormat24H = "dd/MM/yyyy HH:mm:ss.fff";
/// <summary>
/// Cultura italiana per la formattazione (formato 24h di default)
/// </summary>
public static readonly CultureInfo ItalianCulture = new("it-IT");
/// <summary>
/// Converte un TimeSpan in stringa formato 24h (HH:mm)
/// </summary>
public static string FormatTime24H(TimeSpan time)
{
return time.ToString(TimeFormat24H);
}
/// <summary>
/// Converte un DateTime in stringa formato 24h (dd/MM/yyyy HH:mm:ss)
/// </summary>
public static string FormatDateTime24H(DateTime dateTime)
{
return dateTime.ToString(DateTimeFormat24H, ItalianCulture);
}
/// <summary>
/// Converte un DateTime in stringa formato 24h dettagliato con millisecondi
/// </summary>
public static string FormatDateTimeDetailed24H(DateTime dateTime)
{
return dateTime.ToString(DetailedDateTimeFormat24H, ItalianCulture);
}
/// <summary>
/// Prova a parsare una stringa orario in formato 24h
/// </summary>
public static bool TryParseTime24H(string? timeString, out TimeSpan time)
{
time = default;
if (string.IsNullOrWhiteSpace(timeString))
{
return false;
}
return TimeSpan.TryParseExact(timeString.Trim(), TimeFormat24H, ItalianCulture, out time);
}
/// <summary>
/// Prova a parsare una stringa data/ora in formato 24h
/// </summary>
public static bool TryParseDateTime24H(string? dateTimeString, out DateTime dateTime)
{
dateTime = default;
if (string.IsNullOrWhiteSpace(dateTimeString))
{
return false;
}
return DateTime.TryParseExact(dateTimeString.Trim(), DateTimeFormat24H, ItalianCulture, DateTimeStyles.None, out dateTime);
}
/// <summary>
/// Ottiene l'ora corrente locale formattata in 24h
/// </summary>
public static string GetCurrentTime24H()
{
return FormatTime24H(DateTime.Now.TimeOfDay);
}
/// <summary>
/// Ottiene la data/ora corrente locale formattata in 24h
/// </summary>
public static string GetCurrentDateTime24H()
{
return FormatDateTime24H(DateTime.Now);
}
/// <summary>
/// Valida se una stringa rappresenta un orario valido nel formato 24h
/// </summary>
public static bool IsValidTime24H(string? timeString)
{
return TryParseTime24H(timeString, out _);
}
/// <summary>
/// Valida se una stringa rappresenta una data/ora valida nel formato 24h
/// </summary>
public static bool IsValidDateTime24H(string? dateTimeString)
{
return TryParseDateTime24H(dateTimeString, out _);
}
/// <summary>
/// Converte un orario dal formato 12h al formato 24h se necessario
/// </summary>
public static string? ConvertTo24H(string? timeString)
{
if (string.IsNullOrWhiteSpace(timeString))
{
return timeString;
}
// Se è già in formato 24h, restituisci così com'è
if (TryParseTime24H(timeString, out var time24))
{
return FormatTime24H(time24);
}
// Prova a parsare dal formato 12h
if (DateTime.TryParse(timeString.Trim(), ItalianCulture, DateTimeStyles.None, out var parsed))
{
return FormatTime24H(parsed.TimeOfDay);
}
return null; // Formato non riconosciuto
}
/// <summary>
/// Calcola il tempo rimanente fino al prossimo orario schedulato
/// </summary>
public static TimeSpan TimeUntilNextSchedule(TimeSpan scheduledTime, DateTime? referenceTime = null)
{
var now = referenceTime ?? DateTime.Now;
var currentTime = now.TimeOfDay;
// Se l'orario programmato è già passato oggi, programma per domani
if (currentTime > scheduledTime)
{
var tomorrow = now.Date.AddDays(1);
var nextExecution = tomorrow.Add(scheduledTime);
return nextExecution - now;
}
else
{
var today = now.Date;
var nextExecution = today.Add(scheduledTime);
return nextExecution - now;
}
}
/// <summary>
/// Ottiene il nome del giorno della settimana in italiano
/// </summary>
public static string GetDayOfWeekName(DayOfWeek dayOfWeek)
{
return dayOfWeek switch
{
DayOfWeek.Sunday => "Domenica",
DayOfWeek.Monday => "Lunedì",
DayOfWeek.Tuesday => "Martedì",
DayOfWeek.Wednesday => "Mercoledì",
DayOfWeek.Thursday => "Giovedì",
DayOfWeek.Friday => "Venerdì",
DayOfWeek.Saturday => "Sabato",
_ => dayOfWeek.ToString()
};
}
/// <summary>
/// Ottiene il nome del giorno della settimana in italiano tramite indice (0=Domenica)
/// </summary>
public static string GetDayOfWeekName(int dayOfWeekIndex)
{
if (dayOfWeekIndex < 0 || dayOfWeekIndex > 6)
{
return "Non valido";
}
return GetDayOfWeekName((DayOfWeek)dayOfWeekIndex);
}
}
@@ -0,0 +1,24 @@
namespace Data_Coupler.Services;
/// <summary>
/// Risultato dell'esecuzione di un profilo schedulato
/// </summary>
public class ProfileExecutionResult
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public int RecordsProcessed { get; set; }
public DateTime ExecutionTime { get; set; }
public TimeSpan Duration { get; set; }
}
/// <summary>
/// Interfaccia per l'esecuzione di profili schedulati
/// </summary>
public interface IScheduledProfileExecutionService
{
/// <summary>
/// Esegue un profilo Data Coupler specificato dall'ID
/// </summary>
Task<ProfileExecutionResult> ExecuteProfileAsync(int profileId);
}
@@ -0,0 +1,388 @@
using CredentialManager.Models;
using CredentialManager.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Data_Coupler.Services;
/// <summary>
/// Servizio background che gestisce l'esecuzione automatica dei profili schedulati
/// Controlla ogni minuto se ci sono profili da eseguire secondo la schedulazione impostata
/// </summary>
public class ScheduledExecutionBackgroundService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<ScheduledExecutionBackgroundService> _logger;
private readonly TimeSpan _checkInterval = TimeSpan.FromMinutes(1); // Controlla ogni minuto
public ScheduledExecutionBackgroundService(
IServiceProvider serviceProvider,
ILogger<ScheduledExecutionBackgroundService> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Servizio di schedulazione automatica avviato. Controllo ogni {CheckInterval} minuti.",
_checkInterval.TotalMinutes);
while (!stoppingToken.IsCancellationRequested)
{
try
{
await CheckAndExecuteScheduledProfiles();
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore durante il controllo delle schedulazioni");
}
// Attendi il prossimo controllo
await Task.Delay(_checkInterval, stoppingToken);
}
_logger.LogInformation("Servizio di schedulazione automatica arrestato.");
}
private async Task CheckAndExecuteScheduledProfiles()
{
using var scope = _serviceProvider.CreateScope();
var scheduleService = scope.ServiceProvider.GetRequiredService<IProfileScheduleService>();
var executionService = scope.ServiceProvider.GetRequiredService<IScheduledProfileExecutionService>();
var profileService = scope.ServiceProvider.GetRequiredService<IDataCouplerProfileService>();
try
{
// Ottieni tutte le schedulazioni attive
var activeSchedules = await scheduleService.GetActiveSchedulesAsync();
var currentTime = DateTime.Now; // Usa l'ora locale per il confronto con le schedulazioni
_logger.LogDebug("Controllo schedulazioni: {ScheduleCount} schedulazioni attive alle {CurrentTime}",
activeSchedules.Count, DateTimeHelper.FormatDateTime24H(currentTime));
foreach (var schedule in activeSchedules)
{
try
{
if (ShouldExecuteSchedule(schedule, currentTime))
{
_logger.LogInformation("Esecuzione schedulata per profilo: {ProfileName} (Schedule: {ScheduleName})",
schedule.Profile?.Name ?? "N/A", schedule.Name);
// Esegui il profilo
var result = await executionService.ExecuteProfileAsync(schedule.ProfileId);
// Aggiorna la schedulazione
await UpdateScheduleAfterExecution(scheduleService, schedule, currentTime, result.Success);
// Log del risultato
if (result.Success)
{
_logger.LogInformation("Esecuzione schedulata completata con successo per {ProfileName}. " +
"Record processati: {RecordsProcessed}, Durata: {Duration}ms",
schedule.Profile?.Name ?? "N/A", result.RecordsProcessed, result.Duration.TotalMilliseconds);
}
else
{
_logger.LogError("Esecuzione schedulata fallita per {ProfileName}: {ErrorMessage}",
schedule.Profile?.Name ?? "N/A", result.Message);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore durante l'esecuzione della schedulazione {ScheduleName} per il profilo {ProfileName}",
schedule.Name, schedule.Profile?.Name ?? "N/A");
// Aggiorna la schedulazione anche in caso di errore
try
{
await UpdateScheduleAfterExecution(scheduleService, schedule, currentTime, false);
}
catch (Exception updateEx)
{
_logger.LogError(updateEx, "Errore durante l'aggiornamento della schedulazione {ScheduleName} dopo l'errore",
schedule.Name);
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore durante il recupero delle schedulazioni attive");
}
}
/// <summary>
/// Determina se una schedulazione deve essere eseguita in base all'orario corrente
/// </summary>
private bool ShouldExecuteSchedule(ProfileSchedule schedule, DateTime currentTime)
{
if (!schedule.IsEnabled)
{
return false;
}
// Controllo per evitare esecuzioni multiple nella stessa finestra temporale (tolleranza di 1 minuto)
if (schedule.LastExecutionTime.HasValue)
{
var timeSinceLastExecution = currentTime - schedule.LastExecutionTime.Value;
if (timeSinceLastExecution.TotalMinutes < 1)
{
return false;
}
}
return schedule.ScheduleType.ToLower() switch
{
"once" => ShouldExecuteOnceSchedule(schedule, currentTime),
"daily" => ShouldExecuteDailySchedule(schedule, currentTime),
"weekly" => ShouldExecuteWeeklySchedule(schedule, currentTime),
"monthly" => ShouldExecuteMonthlySchedule(schedule, currentTime),
_ => false
};
}
private bool ShouldExecuteOnceSchedule(ProfileSchedule schedule, DateTime currentTime)
{
if (!schedule.ScheduledDateTime.HasValue)
{
_logger.LogWarning("Schedulazione 'once' senza data/ora specificata: {ScheduleName}", schedule.Name);
return false;
}
var scheduledTime = schedule.ScheduledDateTime.Value;
// Esegui se l'orario programmato è passato e non è mai stato eseguito
return currentTime >= scheduledTime && !schedule.LastExecutionTime.HasValue;
}
private bool ShouldExecuteDailySchedule(ProfileSchedule schedule, DateTime currentTime)
{
if (string.IsNullOrEmpty(schedule.DailyTime))
{
_logger.LogWarning("Schedulazione 'daily' senza orario specificato: {ScheduleName}", schedule.Name);
return false;
}
if (!DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime))
{
_logger.LogWarning("Formato orario non valido per schedulazione daily {ScheduleName}: {DailyTime} (formato richiesto: HH:mm)",
schedule.Name, schedule.DailyTime);
return false;
}
var currentTimeOfDay = currentTime.TimeOfDay;
var scheduledTimeOfDay = scheduledTime;
// Verifica se siamo nell'orario giusto (con tolleranza di ±1 minuto)
var timeDifference = Math.Abs((currentTimeOfDay - scheduledTimeOfDay).TotalMinutes);
if (timeDifference > 1)
{
return false;
}
// Se non è mai stato eseguito, esegui
if (!schedule.LastExecutionTime.HasValue)
{
return true;
}
// Se è stato eseguito, verifica che sia passato almeno un giorno
var lastExecutionDate = schedule.LastExecutionTime.Value.Date;
var currentDate = currentTime.Date;
return currentDate > lastExecutionDate;
}
private bool ShouldExecuteWeeklySchedule(ProfileSchedule schedule, DateTime currentTime)
{
if (!schedule.DayOfWeek.HasValue || string.IsNullOrEmpty(schedule.DailyTime))
{
_logger.LogWarning("Schedulazione 'weekly' senza giorno della settimana o orario specificato: {ScheduleName}",
schedule.Name);
return false;
}
if (!DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime))
{
_logger.LogWarning("Formato orario non valido per schedulazione weekly {ScheduleName}: {DailyTime} (formato richiesto: HH:mm)",
schedule.Name, schedule.DailyTime);
return false;
}
// Verifica il giorno della settimana (0=Domenica, 1=Lunedì, etc.)
var currentDayOfWeek = (int)currentTime.DayOfWeek;
if (currentDayOfWeek != schedule.DayOfWeek.Value)
{
return false;
}
// Verifica l'orario (con tolleranza di ±1 minuto)
var currentTimeOfDay = currentTime.TimeOfDay;
var timeDifference = Math.Abs((currentTimeOfDay - scheduledTime).TotalMinutes);
if (timeDifference > 1)
{
return false;
}
// Se non è mai stato eseguito, esegui
if (!schedule.LastExecutionTime.HasValue)
{
return true;
}
// Se è stato eseguito, verifica che sia passata almeno una settimana
var daysSinceLastExecution = (currentTime.Date - schedule.LastExecutionTime.Value.Date).TotalDays;
return daysSinceLastExecution >= 7;
}
private bool ShouldExecuteMonthlySchedule(ProfileSchedule schedule, DateTime currentTime)
{
if (!schedule.DayOfMonth.HasValue || string.IsNullOrEmpty(schedule.DailyTime))
{
_logger.LogWarning("Schedulazione 'monthly' senza giorno del mese o orario specificato: {ScheduleName}",
schedule.Name);
return false;
}
if (!DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime))
{
_logger.LogWarning("Formato orario non valido per schedulazione monthly {ScheduleName}: {DailyTime} (formato richiesto: HH:mm)",
schedule.Name, schedule.DailyTime);
return false;
}
// Verifica il giorno del mese
var currentDayOfMonth = currentTime.Day;
var scheduledDayOfMonth = schedule.DayOfMonth.Value;
// Gestione per mesi con meno giorni (es. 31 in febbraio diventa ultimo giorno del mese)
var daysInCurrentMonth = DateTime.DaysInMonth(currentTime.Year, currentTime.Month);
var effectiveScheduledDay = Math.Min(scheduledDayOfMonth, daysInCurrentMonth);
if (currentDayOfMonth != effectiveScheduledDay)
{
return false;
}
// Verifica l'orario (con tolleranza di ±1 minuto)
var currentTimeOfDay = currentTime.TimeOfDay;
var timeDifference = Math.Abs((currentTimeOfDay - scheduledTime).TotalMinutes);
if (timeDifference > 1)
{
return false;
}
// Se non è mai stato eseguito, esegui
if (!schedule.LastExecutionTime.HasValue)
{
return true;
}
// Se è stato eseguito, verifica che sia passato almeno un mese
var lastExecutionDate = schedule.LastExecutionTime.Value;
var monthsSinceLastExecution = ((currentTime.Year - lastExecutionDate.Year) * 12) + currentTime.Month - lastExecutionDate.Month;
return monthsSinceLastExecution >= 1;
}
/// <summary>
/// Aggiorna la schedulazione dopo l'esecuzione
/// </summary>
private async Task UpdateScheduleAfterExecution(IProfileScheduleService scheduleService,
ProfileSchedule schedule, DateTime executionTime, bool wasSuccessful)
{
try
{
// Aggiorna i dati della schedulazione
schedule.LastExecutionTime = executionTime;
schedule.ExecutionCount++;
// Calcola il prossimo tempo di esecuzione
schedule.NextExecutionTime = CalculateNextExecutionTime(schedule, executionTime);
// Per schedulazioni "once", disabilita dopo l'esecuzione
if (schedule.ScheduleType.ToLower() == "once")
{
schedule.IsEnabled = false;
_logger.LogInformation("Schedulazione 'once' {ScheduleName} disabilitata dopo l'esecuzione", schedule.Name);
}
await scheduleService.UpdateScheduleAsync(schedule);
_logger.LogDebug("Schedulazione {ScheduleName} aggiornata. Prossima esecuzione: {NextExecution}",
schedule.Name, schedule.NextExecutionTime.HasValue ? DateTimeHelper.FormatDateTime24H(schedule.NextExecutionTime.Value) : "N/A");
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore durante l'aggiornamento della schedulazione {ScheduleName}", schedule.Name);
}
}
/// <summary>
/// Calcola il prossimo tempo di esecuzione per una schedulazione
/// </summary>
private DateTime? CalculateNextExecutionTime(ProfileSchedule schedule, DateTime lastExecution)
{
if (!schedule.IsEnabled)
{
return null;
}
return schedule.ScheduleType.ToLower() switch
{
"once" => null, // Schedulazione singola non ha prossima esecuzione
"daily" => CalculateNextDailyExecution(schedule, lastExecution),
"weekly" => CalculateNextWeeklyExecution(schedule, lastExecution),
"monthly" => CalculateNextMonthlyExecution(schedule, lastExecution),
_ => null
};
}
private DateTime? CalculateNextDailyExecution(ProfileSchedule schedule, DateTime lastExecution)
{
if (string.IsNullOrEmpty(schedule.DailyTime) || !DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime))
{
return null;
}
var nextDate = lastExecution.Date.AddDays(1);
return nextDate.Add(scheduledTime);
}
private DateTime? CalculateNextWeeklyExecution(ProfileSchedule schedule, DateTime lastExecution)
{
if (!schedule.DayOfWeek.HasValue || string.IsNullOrEmpty(schedule.DailyTime) ||
!DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime))
{
return null;
}
var nextDate = lastExecution.Date.AddDays(7);
return nextDate.Add(scheduledTime);
}
private DateTime? CalculateNextMonthlyExecution(ProfileSchedule schedule, DateTime lastExecution)
{
if (!schedule.DayOfMonth.HasValue || string.IsNullOrEmpty(schedule.DailyTime) ||
!DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime))
{
return null;
}
var nextMonth = lastExecution.AddMonths(1);
var daysInNextMonth = DateTime.DaysInMonth(nextMonth.Year, nextMonth.Month);
var effectiveDay = Math.Min(schedule.DayOfMonth.Value, daysInNextMonth);
var nextDate = new DateTime(nextMonth.Year, nextMonth.Month, effectiveDay);
return nextDate.Add(scheduledTime);
}
}
File diff suppressed because it is too large Load Diff
+10
View File
@@ -45,6 +45,16 @@
<span class="oi oi-layers" aria-hidden="true"></span> Gestione Profili
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="scheduling">
<span class="oi oi-clock" aria-hidden="true"></span> Schedulazione
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="settings">
<span class="oi oi-cog" aria-hidden="true"></span> Impostazioni
</NavLink>
</div>
</nav>
</div>
+1 -1
View File
@@ -7,4 +7,4 @@
@using Microsoft.JSInterop
@using Data_Coupler
@using Data_Coupler.Shared
@using Components
@using Data_Coupler.Components
+32
View File
@@ -0,0 +1,32 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Data_Coupler.BackgroundServices": "Information",
"System.Net.Http.HttpClient": "Warning"
},
"EventLog": {
"LogLevel": {
"Default": "Warning",
"Data_Coupler": "Information"
}
}
},
"AllowedHosts": "*",
"Urls": "http://*:7550",
"WindowsService": {
"ServiceName": "DataCouplerService",
"DisplayName": "Data Coupler Service",
"Description": "Servizio per l'integrazione e trasferimento dati multi-platform"
},
"Kestrel": {
"Limits": {
"KeepAliveTimeout": "00:10:00",
"RequestHeadersTimeout": "00:05:00",
"MaxConcurrentConnections": 100,
"MaxConcurrentUpgradedConnections": 100
}
}
}
+16 -2
View File
@@ -2,8 +2,22 @@
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"Microsoft.AspNetCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Data_Coupler.BackgroundServices": "Information"
},
"EventLog": {
"LogLevel": {
"Default": "Warning",
"Data_Coupler": "Information"
}
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"Urls": "http://*:7550",
"WindowsService": {
"ServiceName": "DataCouplerService",
"DisplayName": "Data Coupler Service",
"Description": "Servizio per l'integrazione e trasferimento dati multi-platform"
}
}