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,351 @@
using CredentialManager.Data;
using CredentialManager.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace CredentialManager.Services;
public class ProfileScheduleService : IProfileScheduleService
{
private readonly CredentialDbContext _context;
private readonly ILogger<ProfileScheduleService> _logger;
public ProfileScheduleService(CredentialDbContext context, ILogger<ProfileScheduleService> logger)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<List<ProfileSchedule>> GetAllSchedulesAsync()
{
try
{
return await _context.ProfileSchedules
.Include(s => s.Profile)
.OrderBy(s => s.Name)
.ToListAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel recupero di tutte le schedulazioni");
throw;
}
}
public async Task<ProfileSchedule?> GetScheduleByIdAsync(int id)
{
try
{
return await _context.ProfileSchedules
.Include(s => s.Profile)
.FirstOrDefaultAsync(s => s.Id == id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel recupero della schedulazione con ID {Id}", id);
throw;
}
}
public async Task<ProfileSchedule> CreateScheduleAsync(ProfileSchedule schedule)
{
try
{
schedule.CreatedAt = DateTime.UtcNow;
schedule.UpdatedAt = DateTime.UtcNow;
schedule.CreatedBy = Environment.UserName;
// Calcola la prossima esecuzione
schedule.NextExecutionTime = schedule.CalculateNextExecution();
_context.ProfileSchedules.Add(schedule);
await _context.SaveChangesAsync();
_logger.LogInformation("Schedulazione creata: {Name} per il profilo {ProfileId}", schedule.Name, schedule.ProfileId);
return schedule;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella creazione della schedulazione {Name}", schedule.Name);
throw;
}
}
public async Task<ProfileSchedule> UpdateScheduleAsync(ProfileSchedule schedule)
{
try
{
var existingSchedule = await _context.ProfileSchedules.FindAsync(schedule.Id);
if (existingSchedule == null)
throw new InvalidOperationException($"Schedulazione con ID {schedule.Id} non trovata");
// Aggiorna i campi
existingSchedule.Name = schedule.Name;
existingSchedule.Description = schedule.Description;
existingSchedule.ProfileId = schedule.ProfileId;
existingSchedule.IsEnabled = schedule.IsEnabled;
existingSchedule.ScheduleType = schedule.ScheduleType;
existingSchedule.ScheduledDateTime = schedule.ScheduledDateTime;
existingSchedule.DailyTime = schedule.DailyTime;
existingSchedule.DayOfWeek = schedule.DayOfWeek;
existingSchedule.DayOfMonth = schedule.DayOfMonth;
existingSchedule.IntervalValue = schedule.IntervalValue;
existingSchedule.IntervalUnit = schedule.IntervalUnit;
existingSchedule.IsActive = schedule.IsActive;
existingSchedule.UpdatedAt = DateTime.UtcNow;
// Ricalcola la prossima esecuzione
existingSchedule.NextExecutionTime = existingSchedule.CalculateNextExecution();
await _context.SaveChangesAsync();
_logger.LogInformation("Schedulazione aggiornata: {Name}", existingSchedule.Name);
return existingSchedule;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nell'aggiornamento della schedulazione con ID {Id}", schedule.Id);
throw;
}
}
public async Task<bool> DeleteScheduleAsync(int id)
{
try
{
var schedule = await _context.ProfileSchedules.FindAsync(id);
if (schedule == null)
return false;
_context.ProfileSchedules.Remove(schedule);
await _context.SaveChangesAsync();
_logger.LogInformation("Schedulazione eliminata: {Name} (ID: {Id})", schedule.Name, id);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nell'eliminazione della schedulazione con ID {Id}", id);
throw;
}
}
public async Task<List<ProfileSchedule>> GetActiveSchedulesAsync()
{
try
{
return await _context.ProfileSchedules
.Include(s => s.Profile)
.Where(s => s.IsActive && s.IsEnabled)
.OrderBy(s => s.NextExecutionTime)
.ToListAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel recupero delle schedulazioni attive");
throw;
}
}
public async Task<List<ProfileSchedule>> GetPendingExecutionsAsync()
{
try
{
var now = DateTime.Now;
return await _context.ProfileSchedules
.Include(s => s.Profile)
.Where(s => s.IsActive &&
s.IsEnabled &&
s.NextExecutionTime.HasValue &&
s.NextExecutionTime <= now &&
s.LastExecutionStatus != "running")
.OrderBy(s => s.NextExecutionTime)
.ToListAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel recupero delle schedulazioni in attesa di esecuzione");
throw;
}
}
public async Task<bool> UpdateExecutionStatusAsync(int scheduleId, string status, string? message = null, int? recordCount = null)
{
try
{
var schedule = await _context.ProfileSchedules.FindAsync(scheduleId);
if (schedule == null)
return false;
schedule.LastExecutionStatus = status;
schedule.LastExecutionMessage = message;
schedule.LastExecutionTime = DateTime.Now;
if (recordCount.HasValue)
schedule.LastExecutionRecordCount = recordCount;
if (status == "success" || status == "failed")
{
schedule.ExecutionCount++;
// Calcola la prossima esecuzione solo se completata (successo o errore)
schedule.NextExecutionTime = schedule.CalculateNextExecution();
}
await _context.SaveChangesAsync();
_logger.LogInformation("Status esecuzione aggiornato per schedulazione {ScheduleId}: {Status}", scheduleId, status);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nell'aggiornamento dello status per schedulazione {ScheduleId}", scheduleId);
throw;
}
}
public async Task UpdateNextExecutionTimeAsync(int scheduleId)
{
try
{
var schedule = await _context.ProfileSchedules.FindAsync(scheduleId);
if (schedule == null)
return;
// Per schedulazioni a intervallo, calcola dalla ultima esecuzione
if (schedule.ScheduleType == "interval")
{
schedule.NextExecutionTime = schedule.CalculateNextExecutionFromLast();
}
else
{
schedule.NextExecutionTime = schedule.CalculateNextExecution();
}
await _context.SaveChangesAsync();
_logger.LogDebug("Prossima esecuzione aggiornata per schedulazione {ScheduleId}: {NextExecution} (tipo: {Type})",
scheduleId, schedule.NextExecutionTime, schedule.ScheduleType);
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nell'aggiornamento della prossima esecuzione per schedulazione {ScheduleId}", scheduleId);
throw;
}
}
public async Task<List<DataCouplerProfile>> GetAvailableProfilesAsync()
{
try
{
return await _context.DataCouplerProfiles
.Where(p => p.IsActive)
.OrderBy(p => p.Name)
.ToListAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel recupero dei profili disponibili");
throw;
}
}
// Implementazione metodi per lo storico delle esecuzioni
public async Task<ScheduleExecutionHistory> CreateExecutionHistoryAsync(ScheduleExecutionHistory history)
{
try
{
history.CreatedAt = DateTime.UtcNow;
_context.ScheduleExecutionHistories.Add(history);
await _context.SaveChangesAsync();
_logger.LogInformation("Storico esecuzione creato: ID {HistoryId} per schedulazione {ScheduleId}",
history.Id, history.ScheduleId);
return history;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella creazione dello storico esecuzione per schedulazione {ScheduleId}",
history.ScheduleId);
throw;
}
}
public async Task<ScheduleExecutionHistory> UpdateExecutionHistoryAsync(ScheduleExecutionHistory history)
{
try
{
_context.ScheduleExecutionHistories.Update(history);
await _context.SaveChangesAsync();
_logger.LogDebug("Storico esecuzione aggiornato: ID {HistoryId}", history.Id);
return history;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nell'aggiornamento dello storico esecuzione ID {HistoryId}", history.Id);
throw;
}
}
public async Task<List<ScheduleExecutionHistory>> GetExecutionHistoryAsync(int scheduleId, int? limit = null)
{
try
{
var query = _context.ScheduleExecutionHistories
.Where(h => h.ScheduleId == scheduleId)
.OrderByDescending(h => h.StartTime);
if (limit.HasValue)
{
query = (IOrderedQueryable<ScheduleExecutionHistory>)query.Take(limit.Value);
}
return await query.ToListAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel recupero dello storico per schedulazione {ScheduleId}", scheduleId);
throw;
}
}
public async Task<List<ScheduleExecutionHistory>> GetRecentExecutionsAsync(int limit = 50)
{
try
{
return await _context.ScheduleExecutionHistories
.Include(h => h.Schedule)
.OrderByDescending(h => h.StartTime)
.Take(limit)
.ToListAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel recupero delle esecuzioni recenti");
throw;
}
}
public async Task<ScheduleExecutionHistory?> GetExecutionByIdAsync(int executionId)
{
try
{
return await _context.ScheduleExecutionHistories
.Include(h => h.Schedule)
.FirstOrDefaultAsync(h => h.Id == executionId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel recupero dell'esecuzione ID {ExecutionId}", executionId);
throw;
}
}
}