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
+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>