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
+227
View File
@@ -0,0 +1,227 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace CredentialManager.Models;
/// <summary>
/// Modello per la schedulazione dei profili Data Coupler
/// </summary>
public class ProfileSchedule
{
[Key]
public int Id { get; set; }
[Required]
[MaxLength(100)]
public string Name { get; set; } = string.Empty;
[MaxLength(500)]
public string? Description { get; set; }
// Relazione con il profilo
[Required]
public int ProfileId { get; set; }
[ForeignKey(nameof(ProfileId))]
public virtual DataCouplerProfile Profile { get; set; } = null!;
// Configurazione scheduling
[Required]
public bool IsEnabled { get; set; } = true;
[Required]
[MaxLength(20)]
public string ScheduleType { get; set; } = string.Empty; // "once", "daily", "weekly", "monthly", "interval"
public DateTime? ScheduledDateTime { get; set; } // Per schedulazioni "once"
[MaxLength(10)]
public string? DailyTime { get; set; } // Format "HH:mm" per schedulazioni ricorrenti
public int? DayOfWeek { get; set; } // 0-6 per schedulazioni settimanali (0=Domenica)
public int? DayOfMonth { get; set; } // 1-31 per schedulazioni mensili
// Configurazione per schedulazioni a intervalli
public int? IntervalValue { get; set; } // Valore dell'intervallo (es. 5, 10, 30)
[MaxLength(20)]
public string? IntervalUnit { get; set; } // "seconds", "minutes", "hours", "days", "weeks", "months"
// Tracking delle esecuzioni
public DateTime? LastExecutionTime { get; set; }
public DateTime? NextExecutionTime { get; set; }
public int ExecutionCount { get; set; } = 0;
[MaxLength(20)]
public string LastExecutionStatus { get; set; } = string.Empty; // "success", "failed", "running"
[MaxLength(1000)]
public string? LastExecutionMessage { get; set; }
public int? LastExecutionRecordCount { get; set; }
// Configurazione override per database sources
[MaxLength(100)]
public string? SourceDatabaseOverride { get; set; }
[MaxLength(100)]
public string? DestinationDatabaseOverride { get; set; }
// Metadati
[MaxLength(100)]
public string? CreatedBy { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
public bool IsActive { get; set; } = true;
// Metodi helper per calcolare la prossima esecuzione
public DateTime? CalculateNextExecution()
{
if (!IsEnabled || !IsActive)
return null;
var now = DateTime.Now;
return ScheduleType switch
{
"once" => ScheduledDateTime > now ? ScheduledDateTime : null,
"daily" when !string.IsNullOrEmpty(DailyTime) => CalculateNextDaily(now),
"weekly" when DayOfWeek.HasValue && !string.IsNullOrEmpty(DailyTime) => CalculateNextWeekly(now),
"monthly" when DayOfMonth.HasValue && !string.IsNullOrEmpty(DailyTime) => CalculateNextMonthly(now),
"interval" when IntervalValue.HasValue && !string.IsNullOrEmpty(IntervalUnit) => CalculateNextInterval(now),
_ => null
};
}
public DateTime? CalculateNextExecutionFromLast()
{
if (!IsEnabled || !IsActive)
return null;
// Per intervalli, calcola dalla ultima esecuzione (o da ora se mai eseguito)
if (ScheduleType == "interval" && IntervalValue.HasValue && !string.IsNullOrEmpty(IntervalUnit))
{
var baseTime = LastExecutionTime ?? DateTime.Now;
return CalculateNextInterval(baseTime);
}
// Per altri tipi, usa CalculateNextExecution normale
return CalculateNextExecution();
}
private DateTime CalculateNextDaily(DateTime now)
{
var time = TimeSpan.Parse(DailyTime!);
var today = now.Date.Add(time);
return today > now ? today : today.AddDays(1);
}
private DateTime CalculateNextWeekly(DateTime now)
{
var time = TimeSpan.Parse(DailyTime!);
var targetDayOfWeek = (DayOfWeek)DayOfWeek!;
var daysUntilTarget = ((int)targetDayOfWeek - (int)now.DayOfWeek + 7) % 7;
if (daysUntilTarget == 0)
{
// È oggi, controlla se l'orario è già passato
var todayAtTime = now.Date.Add(time);
if (todayAtTime > now)
return todayAtTime;
else
daysUntilTarget = 7; // Prossima settimana
}
return now.Date.AddDays(daysUntilTarget).Add(time);
}
private DateTime CalculateNextMonthly(DateTime now)
{
var time = TimeSpan.Parse(DailyTime!);
var targetDay = DayOfMonth!.Value;
var thisMonth = new DateTime(now.Year, now.Month, Math.Min(targetDay, DateTime.DaysInMonth(now.Year, now.Month))).Add(time);
if (thisMonth > now)
return thisMonth;
// Prossimo mese
var nextMonth = now.AddMonths(1);
return new DateTime(nextMonth.Year, nextMonth.Month, Math.Min(targetDay, DateTime.DaysInMonth(nextMonth.Year, nextMonth.Month))).Add(time);
}
private DateTime CalculateNextInterval(DateTime baseTime)
{
if (!IntervalValue.HasValue || string.IsNullOrEmpty(IntervalUnit))
return baseTime;
return IntervalUnit.ToLower() switch
{
"seconds" => baseTime.AddSeconds(IntervalValue.Value),
"minutes" => baseTime.AddMinutes(IntervalValue.Value),
"hours" => baseTime.AddHours(IntervalValue.Value),
"days" => baseTime.AddDays(IntervalValue.Value),
"weeks" => baseTime.AddDays(IntervalValue.Value * 7),
"months" => baseTime.AddMonths(IntervalValue.Value),
_ => baseTime
};
}
/// <summary>
/// Ottiene una descrizione leggibile della schedulazione
/// </summary>
public string GetScheduleDescription()
{
return ScheduleType switch
{
"once" => $"Una volta il {ScheduledDateTime:dd/MM/yyyy HH:mm}",
"daily" => $"Ogni giorno alle {DailyTime}",
"weekly" => $"Ogni {GetDayOfWeekName(DayOfWeek)} alle {DailyTime}",
"monthly" => $"Il giorno {DayOfMonth} di ogni mese alle {DailyTime}",
"interval" => GetIntervalDescription(),
_ => "Non configurato"
};
}
private string GetIntervalDescription()
{
if (!IntervalValue.HasValue || string.IsNullOrEmpty(IntervalUnit))
return "Intervallo non configurato";
var unit = IntervalUnit.ToLower() switch
{
"seconds" => IntervalValue.Value == 1 ? "secondo" : "secondi",
"minutes" => IntervalValue.Value == 1 ? "minuto" : "minuti",
"hours" => IntervalValue.Value == 1 ? "ora" : "ore",
"days" => IntervalValue.Value == 1 ? "giorno" : "giorni",
"weeks" => IntervalValue.Value == 1 ? "settimana" : "settimane",
"months" => IntervalValue.Value == 1 ? "mese" : "mesi",
_ => IntervalUnit
};
return $"Ogni {IntervalValue} {unit}";
}
private string GetDayOfWeekName(int? dayOfWeek)
{
if (!dayOfWeek.HasValue)
return "sconosciuto";
return ((DayOfWeek)dayOfWeek.Value).ToString() switch
{
"Monday" => "Lunedì",
"Tuesday" => "Martedì",
"Wednesday" => "Mercoledì",
"Thursday" => "Giovedì",
"Friday" => "Venerdì",
"Saturday" => "Sabato",
"Sunday" => "Domenica",
_ => dayOfWeek.Value.ToString()
};
}
}