diff --git a/Common/Constants/Constants.cs b/Common/Constants/Constants.cs
index 2d5d94ce..d6687f25 100644
--- a/Common/Constants/Constants.cs
+++ b/Common/Constants/Constants.cs
@@ -3,6 +3,7 @@
public static class Duration
{
public static readonly TimeSpan AuditRetentionTime = TimeSpan.FromDays(90);
+ public static readonly TimeSpan ShockerControlLogRetentionTime = TimeSpan.FromDays(365);
public static readonly TimeSpan PasswordResetRequestLifetime = TimeSpan.FromHours(1);
diff --git a/Common/Constants/HardLimits.cs b/Common/Constants/HardLimits.cs
index 34c1063d..468218d3 100644
--- a/Common/Constants/HardLimits.cs
+++ b/Common/Constants/HardLimits.cs
@@ -47,4 +47,6 @@ public static class HardLimits
public const int ShockerControlLogCustomNameMaxLength = 64;
public const int CreateShareRequestMaxShockers = 128;
+
+ public const int MaxShockerControlLogsPerUser = 2048;
}
diff --git a/Cron/Jobs/ClearOldShockerControlLogs.cs b/Cron/Jobs/ClearOldShockerControlLogs.cs
new file mode 100644
index 00000000..446a55ea
--- /dev/null
+++ b/Cron/Jobs/ClearOldShockerControlLogs.cs
@@ -0,0 +1,70 @@
+using Microsoft.EntityFrameworkCore;
+using OpenShock.Common;
+using OpenShock.Common.Constants;
+using OpenShock.Common.OpenShockDb;
+using OpenShock.Cron.Attributes;
+
+namespace OpenShock.Cron.Jobs;
+
+///
+/// Deletes shocker control logs older than the retention period and enforces a maximum log count
+///
+[CronJob("0 0 * * *")] // Every day at midnight (https://crontab.guru/)
+public sealed class ClearOldShockerControlLogs
+{
+ private readonly OpenShockContext _db;
+ private readonly ILogger _logger;
+
+ ///
+ /// DI constructor
+ ///
+ ///
+ ///
+ public ClearOldShockerControlLogs(OpenShockContext db, ILogger logger)
+ {
+ _db = db;
+ _logger = logger;
+ }
+
+ public async Task Execute()
+ {
+ // Calculate the retention threshold based on configured retention time.
+ var retentionThreshold = DateTime.UtcNow - Duration.ShockerControlLogRetentionTime;
+
+ // Delete logs older than the retention threshold.
+ var deletedByAge = await _db.ShockerControlLogs
+ .Where(log => log.CreatedOn < retentionThreshold)
+ .ExecuteDeleteAsync();
+
+ _logger.LogInformation("Deleted {deletedCount} shocker control logs older than {retentionThreshold:O}.", deletedByAge, retentionThreshold);
+
+ var userLogsCounts = await _db.ShockerControlLogs
+ .GroupBy(log => log.Shocker.DeviceNavigation.Owner)
+ .Select(group => new
+ {
+ UserId = group.Key,
+ CountToDelete = Math.Max(0, group.Count() - HardLimits.MaxShockerControlLogsPerUser),
+ DeleteBeforeDate = group
+ .OrderByDescending(log => log.CreatedOn)
+ .Skip(HardLimits.MaxShockerControlLogsPerUser)
+ .Select(log => log.CreatedOn)
+ .FirstOrDefault()
+ })
+ .Where(result => result.CountToDelete > 0)
+ .ToArrayAsync();
+
+ if (userLogsCounts.Length != 0)
+ {
+ _logger.LogInformation("A total of {totalLogsToDelete} logs will be deleted to enforce per-user limits.", userLogsCounts.Sum(x => x.CountToDelete));
+
+ foreach (var userLogCount in userLogsCounts)
+ {
+ await _db.ShockerControlLogs
+ .Where(log => log.Shocker.DeviceNavigation.Owner == userLogCount.UserId && log.CreatedOn < userLogCount.DeleteBeforeDate)
+ .ExecuteDeleteAsync();
+ }
+ }
+
+ _logger.LogInformation("Done!");
+ }
+}
\ No newline at end of file