Files
Data-Coupler/Data_Coupler/Pages/Scheduling.razor.cs
Alessio Dal Santo 9fab99112b [Fix] Sicurezza e affidabilità storico esecuzioni schedulazioni
- SchedulingHistory.razor / .cs: iniettato IWebHostEnvironment per nascondere
  lo stack trace (con percorsi di file) in produzione; in produzione viene
  mostrato solo il messaggio di errore sanitizzato e un avviso che invita a
  consultare i log dell'applicazione; in sviluppo il dettaglio completo resta
  visibile invariato.

- Scheduling.razor.cs (ExecuteScheduleManually): isolata la notifica JS
  (ShowSuccessMessage / ShowErrorMessage) in un blocco try-catch separato per
  TaskCanceledException / OperationCanceledException. In questo modo una
  disconnessione del browser durante un'esecuzione lunga non sovrascrive più
  il risultato già salvato correttamente come 'success' con uno stato 'failed'
  e lo stack trace di un'eccezione JSInterop. L'evento viene registrato come
  avviso di log senza impatto sul record storico.

- ScheduledJobService.cs: aggiunto commento esplicativo sul motivo per cui
  il dettaglio completo (ex.ToString) è salvato nel DB ma la UI ne mostra
  solo la versione sanitizzata in produzione.
2026-05-08 13:46:56 +02:00

461 lines
17 KiB
C#

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,
EnableDeletionSync = schedule.EnableDeletionSync
};
// 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,
schedule.EnableDeletionSync);
// 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);
// Notifica l'utente (best-effort: la connessione browser potrebbe essere stata interrotta
// durante un'esecuzione lunga senza che questo invalidi il risultato già salvato).
try
{
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}");
}
catch (OperationCanceledException)
{
// La connessione Blazor è stata interrotta durante l'esecuzione: il risultato è
// già stato salvato correttamente, la notifica non può essere recapitata.
Logger.LogWarning("Notifica UI non inviata per la schedulazione {ScheduleId}: connessione browser interrotta durante l'esecuzione", scheduleId);
}
await LoadSchedules();
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nell'esecuzione manuale schedulazione {ScheduleId}", scheduleId);
// Aggiorna lo storico in caso di eccezione durante l'esecuzione effettiva
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}");
try
{
await ShowErrorMessage("Errore nell'esecuzione: " + ex.Message);
}
catch (OperationCanceledException)
{
Logger.LogWarning("Notifica UI non inviata per la schedulazione {ScheduleId}: connessione browser non disponibile", scheduleId);
}
}
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);
}
}