[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.
This commit is contained in:
Alessio Dal Santo
2026-05-08 13:46:56 +02:00
parent 91dbe9ae11
commit 9fab99112b
4 changed files with 57 additions and 8 deletions
@@ -286,6 +286,8 @@ public class ScheduledJobService : BackgroundService
executionHistory.EndTime = DateTime.Now;
executionHistory.Status = "failed";
executionHistory.Message = $"Errore durante l'esecuzione automatica: {ex.Message}";
// Memorizza il dettaglio completo (stack trace) solo per scopi diagnostici;
// la UI in produzione ne mostrerà una versione sanitizzata senza percorsi di file.
executionHistory.ErrorDetails = ex.ToString();
await scheduleService.UpdateExecutionHistoryAsync(executionHistory);
}
+21 -6
View File
@@ -311,13 +311,20 @@ public partial class Scheduling : ComponentBase
await ScheduleService.UpdateExecutionStatusAsync(scheduleId, status, message, result.RecordsProcessed);
if (result.IsSuccess)
// 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
{
await ShowSuccessMessage($"Schedulazione eseguita con successo! {result.RecordsProcessed} record elaborati in {result.Duration.TotalSeconds:F2} secondi.");
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}");
}
else
catch (OperationCanceledException)
{
await ShowErrorMessage($"Errore durante l'esecuzione: {result.ErrorMessage}");
// 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();
@@ -326,7 +333,7 @@ public partial class Scheduling : ComponentBase
{
Logger.LogError(ex, "Errore nell'esecuzione manuale schedulazione {ScheduleId}", scheduleId);
// Aggiorna lo storico in caso di eccezione
// Aggiorna lo storico in caso di eccezione durante l'esecuzione effettiva
if (executionHistory != null)
{
executionHistory.EndTime = DateTime.Now;
@@ -337,7 +344,15 @@ public partial class Scheduling : ComponentBase
}
await ScheduleService.UpdateExecutionStatusAsync(scheduleId, "failed", $"Errore: {ex.Message}");
await ShowErrorMessage("Errore nell'esecuzione: " + 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
{
+9 -1
View File
@@ -235,7 +235,15 @@
{
<h6>Dettagli Errori</h6>
<div class="alert alert-danger">
<pre style="white-space: pre-wrap; font-size: 0.85em;">@selectedExecution.ErrorDetails</pre>
@if (IsDevelopment)
{
<pre style="white-space: pre-wrap; font-size: 0.85em;">@selectedExecution.ErrorDetails</pre>
}
else
{
<p class="mb-1">@GetSanitizedErrorMessage(selectedExecution.ErrorDetails)</p>
<small class="text-muted"><i class="fas fa-info-circle"></i> Per i dettagli tecnici completi consultare i log dell'applicazione.</small>
}
</div>
}
@@ -1,6 +1,7 @@
using CredentialManager.Models;
using CredentialManager.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
@@ -11,6 +12,29 @@ 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!;
[Inject] private IWebHostEnvironment WebHostEnvironment { get; set; } = null!;
protected bool IsDevelopment => WebHostEnvironment.IsDevelopment();
/// <summary>
/// Restituisce solo il messaggio dell'eccezione (senza stack trace) per la visualizzazione in produzione.
/// </summary>
protected static string GetSanitizedErrorMessage(string errorDetails)
{
if (string.IsNullOrEmpty(errorDetails))
return string.Empty;
// Prende solo le righe fino al primo stack frame (riga che inizia con " at")
var lines = errorDetails.Split('\n');
var messageLines = new System.Collections.Generic.List<string>();
foreach (var line in lines)
{
if (line.TrimStart().StartsWith("at ", StringComparison.Ordinal))
break;
messageLines.Add(line.TrimEnd());
}
return string.Join("\n", messageLines).Trim();
}
protected List<ScheduleExecutionHistory>? executionHistory;
protected ScheduleExecutionHistory? selectedExecution;