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:
@@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace CredentialManager.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Modello per lo storico delle esecuzioni delle schedulazioni
|
||||
/// </summary>
|
||||
public class ScheduleExecutionHistory
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
// Relazione con la schedulazione
|
||||
[Required]
|
||||
public int ScheduleId { get; set; }
|
||||
|
||||
[ForeignKey(nameof(ScheduleId))]
|
||||
public virtual ProfileSchedule Schedule { get; set; } = null!;
|
||||
|
||||
// Relazione con il profilo (denormalizzato per storico)
|
||||
[Required]
|
||||
public int ProfileId { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(200)]
|
||||
public string ProfileName { get; set; } = string.Empty;
|
||||
|
||||
// Informazioni dell'esecuzione
|
||||
[Required]
|
||||
public DateTime StartTime { get; set; }
|
||||
|
||||
public DateTime? EndTime { get; set; }
|
||||
|
||||
public TimeSpan? Duration => EndTime - StartTime;
|
||||
|
||||
[Required]
|
||||
[MaxLength(20)]
|
||||
public string Status { get; set; } = string.Empty; // "success", "failed", "running", "cancelled"
|
||||
|
||||
[MaxLength(2000)]
|
||||
public string? Message { get; set; }
|
||||
|
||||
public int RecordsProcessed { get; set; } = 0;
|
||||
|
||||
public int? RecordsWithErrors { get; set; }
|
||||
|
||||
// Dettagli dell'errore se presente
|
||||
[MaxLength(5000)]
|
||||
public string? ErrorDetails { get; set; }
|
||||
|
||||
// Informazioni sul trigger
|
||||
[Required]
|
||||
[MaxLength(20)]
|
||||
public string TriggerType { get; set; } = string.Empty; // "manual", "automatic"
|
||||
|
||||
[MaxLength(100)]
|
||||
public string? TriggeredBy { get; set; }
|
||||
|
||||
// Configurazioni al momento dell'esecuzione (per storico)
|
||||
[MaxLength(50)]
|
||||
public string? SourceType { get; set; }
|
||||
|
||||
[MaxLength(50)]
|
||||
public string? DestinationType { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? SourceInfo { get; set; } // Informazioni aggiuntive sulla sorgente utilizzata
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? DestinationInfo { get; set; } // Informazioni aggiuntive sulla destinazione utilizzata
|
||||
|
||||
// Metadati
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[MaxLength(2000)]
|
||||
public string? AdditionalInfo { get; set; } // JSON per informazioni aggiuntive
|
||||
|
||||
// Helper methods
|
||||
public bool IsCompleted => Status == "success" || Status == "failed" || Status == "cancelled";
|
||||
|
||||
public bool IsSuccessful => Status == "success";
|
||||
|
||||
public string GetStatusDisplayText() => Status switch
|
||||
{
|
||||
"success" => "Completato con successo",
|
||||
"failed" => "Fallito",
|
||||
"running" => "In esecuzione",
|
||||
"cancelled" => "Cancellato",
|
||||
_ => "Sconosciuto"
|
||||
};
|
||||
|
||||
public string GetStatusColorClass() => Status switch
|
||||
{
|
||||
"success" => "text-success",
|
||||
"failed" => "text-danger",
|
||||
"running" => "text-primary",
|
||||
"cancelled" => "text-warning",
|
||||
_ => "text-muted"
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user