From 45cb384fc705358d101f00df05fa586a81d58dc8 Mon Sep 17 00:00:00 2001 From: Flaminel Date: Tue, 24 Mar 2026 22:06:21 +0200 Subject: [PATCH] fixed cycle id naming inconsistency --- .../Contracts/Responses/InstanceSearchStat.cs | 2 +- .../Responses/SearchEventResponse.cs | 2 +- .../Controllers/SearchStatsController.cs | 18 ++--- .../Events/EventPublisherTests.cs | 8 +-- .../Features/Jobs/SeekerTests.cs | 68 +++++++++---------- .../Events/EventPublisher.cs | 6 +- .../Events/Interfaces/IEventPublisher.cs | 2 +- .../Features/Jobs/Seeker.cs | 26 +++---- .../Cleanuparr.Persistence/DataContext.cs | 2 +- .../Seeker/SeekerInstanceConfig.cs | 4 +- .../Models/Events/AppEvent.cs | 6 +- .../Models/State/SeekerHistory.cs | 6 +- .../src/app/core/api/search-stats.api.ts | 4 +- .../app/core/models/search-stats.models.ts | 4 +- .../search-stats/search-stats.component.html | 10 +-- .../search-stats/search-stats.component.ts | 6 +- 16 files changed, 87 insertions(+), 87 deletions(-) diff --git a/code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Responses/InstanceSearchStat.cs b/code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Responses/InstanceSearchStat.cs index dd7ddd79..8e2d796c 100644 --- a/code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Responses/InstanceSearchStat.cs +++ b/code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Responses/InstanceSearchStat.cs @@ -9,7 +9,7 @@ public sealed record InstanceSearchStat public int TotalSearchCount { get; init; } public DateTime? LastSearchedAt { get; init; } public DateTime? LastProcessedAt { get; init; } - public Guid? CurrentRunId { get; init; } + public Guid? CurrentCycleId { get; init; } public int CycleItemsSearched { get; init; } public int CycleItemsTotal { get; init; } public DateTime? CycleStartedAt { get; init; } diff --git a/code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Responses/SearchEventResponse.cs b/code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Responses/SearchEventResponse.cs index 2743200b..318bee68 100644 --- a/code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Responses/SearchEventResponse.cs +++ b/code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Responses/SearchEventResponse.cs @@ -14,6 +14,6 @@ public sealed record SearchEventResponse public SearchCommandStatus? SearchStatus { get; init; } public DateTime? CompletedAt { get; init; } public object? GrabbedItems { get; init; } - public Guid? CycleRunId { get; init; } + public Guid? CycleId { get; init; } public bool IsDryRun { get; init; } } diff --git a/code/backend/Cleanuparr.Api/Features/Seeker/Controllers/SearchStatsController.cs b/code/backend/Cleanuparr.Api/Features/Seeker/Controllers/SearchStatsController.cs index 86b51eb3..dbb2a810 100644 --- a/code/backend/Cleanuparr.Api/Features/Seeker/Controllers/SearchStatsController.cs +++ b/code/backend/Cleanuparr.Api/Features/Seeker/Controllers/SearchStatsController.cs @@ -72,10 +72,10 @@ public sealed class SearchStatsController : ControllerBase .ToListAsync(); // Count items searched in current cycle per instance - List currentRunIds = instanceConfigs.Select(ic => ic.CurrentRunId).ToList(); + List currentCycleIds = instanceConfigs.Select(ic => ic.CurrentCycleId).ToList(); var cycleItemsByInstance = await _dataContext.SeekerHistory .AsNoTracking() - .Where(h => currentRunIds.Contains(h.RunId)) + .Where(h => currentCycleIds.Contains(h.CycleId)) .GroupBy(h => h.ArrInstanceId) .Select(g => new { @@ -98,7 +98,7 @@ public sealed class SearchStatsController : ControllerBase TotalSearchCount = history?.TotalSearchCount ?? 0, LastSearchedAt = history?.LastSearchedAt, LastProcessedAt = ic.LastProcessedAt, - CurrentRunId = ic.CurrentRunId, + CurrentCycleId = ic.CurrentCycleId, CycleItemsSearched = cycleProgress?.CycleItemsSearched ?? 0, CycleItemsTotal = ic.TotalEligibleItems, CycleStartedAt = cycleProgress?.CycleStartedAt, @@ -205,7 +205,7 @@ public sealed class SearchStatsController : ControllerBase [FromQuery] int page = 1, [FromQuery] int pageSize = 50, [FromQuery] Guid? instanceId = null, - [FromQuery] Guid? cycleRunId = null) + [FromQuery] Guid? cycleId = null) { if (page < 1) page = 1; if (pageSize < 1) pageSize = 50; @@ -229,10 +229,10 @@ public sealed class SearchStatsController : ControllerBase } } - // Filter by cycle run ID - if (cycleRunId.HasValue) + // Filter by cycle ID + if (cycleId.HasValue) { - query = query.Where(e => e.CycleRunId == cycleRunId.Value); + query = query.Where(e => e.CycleId == cycleId.Value); } int totalCount = await query.CountAsync(); @@ -258,7 +258,7 @@ public sealed class SearchStatsController : ControllerBase SearchStatus = e.SearchStatus, CompletedAt = e.CompletedAt, GrabbedItems = parsed.GrabbedItems, - CycleRunId = e.CycleRunId, + CycleId = e.CycleId, IsDryRun = e.IsDryRun, }; }).ToList(); @@ -344,7 +344,7 @@ public sealed class SearchStatsController : ControllerBase SearchStatus = e.SearchStatus, CompletedAt = e.CompletedAt, GrabbedItems = parsed.GrabbedItems, - CycleRunId = e.CycleRunId, + CycleId = e.CycleId, IsDryRun = e.IsDryRun, } : null; diff --git a/code/backend/Cleanuparr.Infrastructure.Tests/Events/EventPublisherTests.cs b/code/backend/Cleanuparr.Infrastructure.Tests/Events/EventPublisherTests.cs index b4f1465a..4f8932df 100644 --- a/code/backend/Cleanuparr.Infrastructure.Tests/Events/EventPublisherTests.cs +++ b/code/backend/Cleanuparr.Infrastructure.Tests/Events/EventPublisherTests.cs @@ -608,18 +608,18 @@ public class EventPublisherTests : IDisposable } [Fact] - public async Task PublishSearchTriggered_SetsCycleRunId() + public async Task PublishSearchTriggered_SetsCycleId() { // Arrange - var cycleRunId = Guid.NewGuid(); + var cycleId = Guid.NewGuid(); // Act - await _publisher.PublishSearchTriggered("Radarr-1", 1, ["Movie A"], SeekerSearchType.Proactive, cycleRunId); + await _publisher.PublishSearchTriggered("Radarr-1", 1, ["Movie A"], SeekerSearchType.Proactive, cycleId); // Assert var savedEvent = await _context.Events.FirstOrDefaultAsync(); Assert.NotNull(savedEvent); - Assert.Equal(cycleRunId, savedEvent.CycleRunId); + Assert.Equal(cycleId, savedEvent.CycleId); } [Fact] diff --git a/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/SeekerTests.cs b/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/SeekerTests.cs index c7490253..7984dfce 100644 --- a/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/SeekerTests.cs +++ b/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/SeekerTests.cs @@ -717,7 +717,7 @@ public class SeekerTests : IDisposable await _fixture.DataContext.SaveChangesAsync(); var radarrInstance = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext); - var currentRunId = Guid.NewGuid(); + var currentCycleId = Guid.NewGuid(); var now = _fixture.TimeProvider.GetUtcNow().UtcDateTime; _fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig @@ -725,7 +725,7 @@ public class SeekerTests : IDisposable ArrInstanceId = radarrInstance.Id, ArrInstance = radarrInstance, Enabled = true, - CurrentRunId = currentRunId + CurrentCycleId = currentCycleId }); // Add history entries for both movies in the current cycle @@ -735,7 +735,7 @@ public class SeekerTests : IDisposable ArrInstanceId = radarrInstance.Id, ExternalItemId = 1, ItemType = InstanceType.Radarr, - RunId = currentRunId, + CycleId = currentCycleId, LastSearchedAt = now.AddDays(-10), ItemTitle = "Movie 1" }); @@ -744,7 +744,7 @@ public class SeekerTests : IDisposable ArrInstanceId = radarrInstance.Id, ExternalItemId = 2, ItemType = InstanceType.Radarr, - RunId = currentRunId, + CycleId = currentCycleId, LastSearchedAt = now.AddDays(-10), ItemTitle = "Movie 2" }); @@ -777,14 +777,14 @@ public class SeekerTests : IDisposable // Act await sut.ExecuteAsync(); - // Assert — search was triggered (new cycle started) and the RunId changed + // Assert — search was triggered (new cycle started) and the CycleId changed mockArrClient.Verify( x => x.SearchItemsAsync(radarrInstance, It.IsAny>()), Times.Once); var instanceConfig = await _fixture.DataContext.SeekerInstanceConfigs .FirstAsync(s => s.ArrInstanceId == radarrInstance.Id); - Assert.NotEqual(currentRunId, instanceConfig.CurrentRunId); + Assert.NotEqual(currentCycleId, instanceConfig.CurrentCycleId); } #endregion @@ -920,7 +920,7 @@ public class SeekerTests : IDisposable await _fixture.DataContext.SaveChangesAsync(); var radarrInstance = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext); - var currentRunId = Guid.NewGuid(); + var currentCycleId = Guid.NewGuid(); var now = _fixture.TimeProvider.GetUtcNow().UtcDateTime; _fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig @@ -928,7 +928,7 @@ public class SeekerTests : IDisposable ArrInstanceId = radarrInstance.Id, ArrInstance = radarrInstance, Enabled = true, - CurrentRunId = currentRunId, + CurrentCycleId = currentCycleId, MinCycleTimeDays = 7, TotalEligibleItems = 2 }); @@ -939,7 +939,7 @@ public class SeekerTests : IDisposable ArrInstanceId = radarrInstance.Id, ExternalItemId = 1, ItemType = InstanceType.Radarr, - RunId = currentRunId, + CycleId = currentCycleId, LastSearchedAt = now.AddDays(-2), ItemTitle = "Movie 1" }); @@ -948,7 +948,7 @@ public class SeekerTests : IDisposable ArrInstanceId = radarrInstance.Id, ExternalItemId = 2, ItemType = InstanceType.Radarr, - RunId = currentRunId, + CycleId = currentCycleId, LastSearchedAt = now.AddDays(-1), ItemTitle = "Movie 2" }); @@ -984,7 +984,7 @@ public class SeekerTests : IDisposable var instanceConfig = await _fixture.DataContext.SeekerInstanceConfigs .FirstAsync(s => s.ArrInstanceId == radarrInstance.Id); - Assert.Equal(currentRunId, instanceConfig.CurrentRunId); + Assert.Equal(currentCycleId, instanceConfig.CurrentCycleId); } [Fact] @@ -998,7 +998,7 @@ public class SeekerTests : IDisposable await _fixture.DataContext.SaveChangesAsync(); var radarrInstance = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext); - var currentRunId = Guid.NewGuid(); + var currentCycleId = Guid.NewGuid(); var now = _fixture.TimeProvider.GetUtcNow().UtcDateTime; _fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig @@ -1006,7 +1006,7 @@ public class SeekerTests : IDisposable ArrInstanceId = radarrInstance.Id, ArrInstance = radarrInstance, Enabled = true, - CurrentRunId = currentRunId, + CurrentCycleId = currentCycleId, MinCycleTimeDays = 7, TotalEligibleItems = 2 }); @@ -1017,7 +1017,7 @@ public class SeekerTests : IDisposable ArrInstanceId = radarrInstance.Id, ExternalItemId = 1, ItemType = InstanceType.Radarr, - RunId = currentRunId, + CycleId = currentCycleId, LastSearchedAt = now.AddDays(-10), ItemTitle = "Movie 1" }); @@ -1026,7 +1026,7 @@ public class SeekerTests : IDisposable ArrInstanceId = radarrInstance.Id, ExternalItemId = 2, ItemType = InstanceType.Radarr, - RunId = currentRunId, + CycleId = currentCycleId, LastSearchedAt = now.AddDays(-8), ItemTitle = "Movie 2" }); @@ -1066,7 +1066,7 @@ public class SeekerTests : IDisposable var instanceConfig = await _fixture.DataContext.SeekerInstanceConfigs .FirstAsync(s => s.ArrInstanceId == radarrInstance.Id); - Assert.NotEqual(currentRunId, instanceConfig.CurrentRunId); + Assert.NotEqual(currentCycleId, instanceConfig.CurrentCycleId); } [Fact] @@ -1080,25 +1080,25 @@ public class SeekerTests : IDisposable await _fixture.DataContext.SaveChangesAsync(); var radarrInstance = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext); - var currentRunId = Guid.NewGuid(); + var currentCycleId = Guid.NewGuid(); _fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig { ArrInstanceId = radarrInstance.Id, ArrInstance = radarrInstance, Enabled = true, - CurrentRunId = currentRunId, + CurrentCycleId = currentCycleId, MinCycleTimeDays = 30 }); - // History uses a DIFFERENT RunId — current cycle has no history entries - var oldRunId = Guid.NewGuid(); + // History uses a DIFFERENT CycleId — current cycle has no history entries + var oldCycleId = Guid.NewGuid(); _fixture.DataContext.SeekerHistory.Add(new SeekerHistory { ArrInstanceId = radarrInstance.Id, ExternalItemId = 1, ItemType = InstanceType.Radarr, - RunId = oldRunId, + CycleId = oldCycleId, LastSearchedAt = DateTime.UtcNow.AddDays(-60), ItemTitle = "Movie 1" }); @@ -1147,7 +1147,7 @@ public class SeekerTests : IDisposable await _fixture.DataContext.SaveChangesAsync(); var sonarrInstance = TestDataContextFactory.AddSonarrInstance(_fixture.DataContext); - var currentRunId = Guid.NewGuid(); + var currentCycleId = Guid.NewGuid(); var now = _fixture.TimeProvider.GetUtcNow().UtcDateTime; _fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig @@ -1155,7 +1155,7 @@ public class SeekerTests : IDisposable ArrInstanceId = sonarrInstance.Id, ArrInstance = sonarrInstance, Enabled = true, - CurrentRunId = currentRunId, + CurrentCycleId = currentCycleId, MinCycleTimeDays = 7, TotalEligibleItems = 1 }); @@ -1167,7 +1167,7 @@ public class SeekerTests : IDisposable ExternalItemId = 10, ItemType = InstanceType.Sonarr, SeasonNumber = 1, - RunId = currentRunId, + CycleId = currentCycleId, LastSearchedAt = now.AddDays(-2), ItemTitle = "Test Series" }); @@ -1210,7 +1210,7 @@ public class SeekerTests : IDisposable var instanceConfig = await _fixture.DataContext.SeekerInstanceConfigs .FirstAsync(s => s.ArrInstanceId == sonarrInstance.Id); - Assert.Equal(currentRunId, instanceConfig.CurrentRunId); + Assert.Equal(currentCycleId, instanceConfig.CurrentCycleId); } [Fact] @@ -1224,7 +1224,7 @@ public class SeekerTests : IDisposable await _fixture.DataContext.SaveChangesAsync(); var sonarrInstance = TestDataContextFactory.AddSonarrInstance(_fixture.DataContext); - var currentRunId = Guid.NewGuid(); + var currentCycleId = Guid.NewGuid(); var now = _fixture.TimeProvider.GetUtcNow().UtcDateTime; _fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig @@ -1232,7 +1232,7 @@ public class SeekerTests : IDisposable ArrInstanceId = sonarrInstance.Id, ArrInstance = sonarrInstance, Enabled = true, - CurrentRunId = currentRunId, + CurrentCycleId = currentCycleId, MinCycleTimeDays = 7, TotalEligibleItems = 1 }); @@ -1244,7 +1244,7 @@ public class SeekerTests : IDisposable ExternalItemId = 10, ItemType = InstanceType.Sonarr, SeasonNumber = 1, - RunId = currentRunId, + CycleId = currentCycleId, LastSearchedAt = now.AddDays(-10), ItemTitle = "Test Series" }); @@ -1291,7 +1291,7 @@ public class SeekerTests : IDisposable var instanceConfig = await _fixture.DataContext.SeekerInstanceConfigs .FirstAsync(s => s.ArrInstanceId == sonarrInstance.Id); - Assert.NotEqual(currentRunId, instanceConfig.CurrentRunId); + Assert.NotEqual(currentCycleId, instanceConfig.CurrentCycleId); } [Fact] @@ -1309,13 +1309,13 @@ public class SeekerTests : IDisposable // Instance A: cycle complete, waiting for MinCycleTimeDays (oldest LastProcessedAt — would be picked first) var instanceA = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext, "http://radarr-a:7878"); - var runIdA = Guid.NewGuid(); + var cycleIdA = Guid.NewGuid(); _fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig { ArrInstanceId = instanceA.Id, ArrInstance = instanceA, Enabled = true, - CurrentRunId = runIdA, + CurrentCycleId = cycleIdA, MinCycleTimeDays = 30, TotalEligibleItems = 1, LastProcessedAt = now.AddDays(-5) // Oldest — round-robin would pick this first @@ -1325,20 +1325,20 @@ public class SeekerTests : IDisposable ArrInstanceId = instanceA.Id, ExternalItemId = 1, ItemType = InstanceType.Radarr, - RunId = runIdA, + CycleId = cycleIdA, LastSearchedAt = now.AddDays(-2), // Cycle started 2 days ago, MinCycleTimeDays=30 ItemTitle = "Movie A" }); // Instance B: has work to do (newer LastProcessedAt) var instanceB = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext, "http://radarr-b:7878"); - var runIdB = Guid.NewGuid(); + var cycleIdB = Guid.NewGuid(); _fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig { ArrInstanceId = instanceB.Id, ArrInstance = instanceB, Enabled = true, - CurrentRunId = runIdB, + CurrentCycleId = cycleIdB, MinCycleTimeDays = 5, TotalEligibleItems = 1, LastProcessedAt = now.AddDays(-1) diff --git a/code/backend/Cleanuparr.Infrastructure/Events/EventPublisher.cs b/code/backend/Cleanuparr.Infrastructure/Events/EventPublisher.cs index c17d4290..eade5573 100644 --- a/code/backend/Cleanuparr.Infrastructure/Events/EventPublisher.cs +++ b/code/backend/Cleanuparr.Infrastructure/Events/EventPublisher.cs @@ -231,7 +231,7 @@ public class EventPublisher : IEventPublisher /// Publishes a search triggered event with context data and notifications. /// Returns the event ID so the SeekerCommandMonitor can update it on completion. /// - public async Task PublishSearchTriggered(string instanceName, int itemCount, IEnumerable items, SeekerSearchType searchType, Guid? cycleRunId = null) + public async Task PublishSearchTriggered(string instanceName, int itemCount, IEnumerable items, SeekerSearchType searchType, Guid? cycleId = null) { var itemList = items as string[] ?? items.ToArray(); var itemsDisplay = string.Join(", ", itemList.Take(5)) + (itemList.Length > 5 ? $" (+{itemList.Length - 5} more)" : ""); @@ -242,7 +242,7 @@ public class EventPublisher : IEventPublisher Message = $"Searched {itemCount} items on {instanceName}: {itemsDisplay}", Severity = EventSeverity.Information, Data = JsonSerializer.Serialize( - new { InstanceName = instanceName, ItemCount = itemCount, Items = itemList, SearchType = searchType.ToString(), CycleRunId = cycleRunId }, + new { InstanceName = instanceName, ItemCount = itemCount, Items = itemList, SearchType = searchType.ToString(), CycleId = cycleId }, new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } }), SearchStatus = SearchCommandStatus.Pending, JobRunId = ContextProvider.TryGetJobRunId(), @@ -250,7 +250,7 @@ public class EventPublisher : IEventPublisher InstanceUrl = (ContextProvider.Get(ContextProvider.Keys.ArrInstanceUrl) as Uri)?.ToString(), DownloadClientType = ContextProvider.Get(ContextProvider.Keys.DownloadClientType) is DownloadClientTypeName dct ? dct : null, DownloadClientName = ContextProvider.Get(ContextProvider.Keys.DownloadClientName) as string, - CycleRunId = cycleRunId, + CycleId = cycleId, }; eventEntity.IsDryRun = await _dryRunInterceptor.IsDryRunEnabled(); diff --git a/code/backend/Cleanuparr.Infrastructure/Events/Interfaces/IEventPublisher.cs b/code/backend/Cleanuparr.Infrastructure/Events/Interfaces/IEventPublisher.cs index 03e671b9..a691aae8 100644 --- a/code/backend/Cleanuparr.Infrastructure/Events/Interfaces/IEventPublisher.cs +++ b/code/backend/Cleanuparr.Infrastructure/Events/Interfaces/IEventPublisher.cs @@ -20,7 +20,7 @@ public interface IEventPublisher Task PublishSearchNotTriggered(string hash, string itemName); - Task PublishSearchTriggered(string instanceName, int itemCount, IEnumerable items, SeekerSearchType searchType, Guid? cycleRunId = null); + Task PublishSearchTriggered(string instanceName, int itemCount, IEnumerable items, SeekerSearchType searchType, Guid? cycleId = null); Task PublishSearchCompleted(Guid eventId, SearchCommandStatus status, object? resultData = null); } \ No newline at end of file diff --git a/code/backend/Cleanuparr.Infrastructure/Features/Jobs/Seeker.cs b/code/backend/Cleanuparr.Infrastructure/Features/Jobs/Seeker.cs index 732639d6..a81f6ced 100644 --- a/code/backend/Cleanuparr.Infrastructure/Features/Jobs/Seeker.cs +++ b/code/backend/Cleanuparr.Infrastructure/Features/Jobs/Seeker.cs @@ -315,7 +315,7 @@ public sealed class Seeker : IHandler // Load search history for the current cycle List currentCycleHistory = await _dataContext.SeekerHistory .AsNoTracking() - .Where(h => h.ArrInstanceId == arrInstance.Id && h.RunId == instanceConfig.CurrentRunId) + .Where(h => h.ArrInstanceId == arrInstance.Id && h.CycleId == instanceConfig.CurrentCycleId) .ToListAsync(); // Load all history for stale cleanup @@ -379,13 +379,13 @@ public sealed class Seeker : IHandler List commandIds = await arrClient.SearchItemsAsync(arrInstance, searchItems); // Publish event (always saved, flagged with IsDryRun in EventPublisher) - Guid eventId = await _eventPublisher.PublishSearchTriggered(arrInstance.Name, searchItems.Count, selectedNames, SeekerSearchType.Proactive, instanceConfig.CurrentRunId); + Guid eventId = await _eventPublisher.PublishSearchTriggered(arrInstance.Name, searchItems.Count, selectedNames, SeekerSearchType.Proactive, instanceConfig.CurrentCycleId); _logger.LogInformation("Searched {Count} items on {InstanceName}: {Items}", searchItems.Count, arrInstance.Name, string.Join(", ", selectedNames)); // Update search history (always, so stats are accurate during dry run) - await UpdateSearchHistoryAsync(arrInstance.Id, instanceType, instanceConfig.CurrentRunId, historyIds, selectedNames, seasonNumber, isDryRun); + await UpdateSearchHistoryAsync(arrInstance.Id, instanceType, instanceConfig.CurrentCycleId, historyIds, selectedNames, seasonNumber, isDryRun); if (!isDryRun) { @@ -396,7 +396,7 @@ public sealed class Seeker : IHandler // Cleanup stale history entries and old cycle history await CleanupStaleHistoryAsync(arrInstance.Id, instanceType, allLibraryIds, allHistoryExternalIds); - await CleanupOldCycleHistoryAsync(arrInstance, instanceConfig.CurrentRunId); + await CleanupOldCycleHistoryAsync(arrInstance, instanceConfig.CurrentCycleId); } } @@ -470,7 +470,7 @@ public sealed class Seeker : IHandler if (!isDryRun) { - instanceConfig.CurrentRunId = Guid.NewGuid(); + instanceConfig.CurrentCycleId = Guid.NewGuid(); _dataContext.SeekerInstanceConfigs.Update(instanceConfig); await _dataContext.SaveChangesAsync(); } @@ -587,7 +587,7 @@ public sealed class Seeker : IHandler candidates.Count, arrInstance.Name); if (!isDryRun) { - instanceConfig.CurrentRunId = Guid.NewGuid(); + instanceConfig.CurrentCycleId = Guid.NewGuid(); _dataContext.SeekerInstanceConfigs.Update(instanceConfig); await _dataContext.SaveChangesAsync(); } @@ -736,7 +736,7 @@ public sealed class Seeker : IHandler private async Task UpdateSearchHistoryAsync( Guid arrInstanceId, InstanceType instanceType, - Guid runId, + Guid cycleId, List searchedIds, List? itemTitles = null, int seasonNumber = 0, @@ -755,7 +755,7 @@ public sealed class Seeker : IHandler && h.ExternalItemId == id && h.ItemType == instanceType && h.SeasonNumber == seasonNumber - && h.RunId == runId); + && h.CycleId == cycleId); if (existing is not null) { @@ -774,7 +774,7 @@ public sealed class Seeker : IHandler ExternalItemId = id, ItemType = instanceType, SeasonNumber = seasonNumber, - RunId = runId, + CycleId = cycleId, LastSearchedAt = now, ItemTitle = title, IsDryRun = isDryRun, @@ -850,13 +850,13 @@ public sealed class Seeker : IHandler /// Removes history entries from previous cycles that are older than 30 days. /// Recent cycle history is retained for statistics and history viewing. /// - private async Task CleanupOldCycleHistoryAsync(ArrInstance arrInstance, Guid currentRunId) + private async Task CleanupOldCycleHistoryAsync(ArrInstance arrInstance, Guid currentCycleId) { DateTime cutoff = _timeProvider.GetUtcNow().UtcDateTime.AddDays(-30); int deleted = await _dataContext.SeekerHistory .Where(h => h.ArrInstanceId == arrInstance.Id - && h.RunId != currentRunId + && h.CycleId != currentCycleId && h.LastSearchedAt < cutoff) .ExecuteDeleteAsync(); @@ -877,7 +877,7 @@ public sealed class Seeker : IHandler // Count distinct items searched in current cycle int cycleItemsSearched = await _dataContext.SeekerHistory .AsNoTracking() - .Where(h => h.ArrInstanceId == ic.ArrInstanceId && h.RunId == ic.CurrentRunId) + .Where(h => h.ArrInstanceId == ic.ArrInstanceId && h.CycleId == ic.CurrentCycleId) .Select(h => h.ExternalItemId) .Distinct() .CountAsync(); @@ -899,7 +899,7 @@ public sealed class Seeker : IHandler // Cycle is complete, but check if min time has elapsed DateTime? cycleStartedAt = await _dataContext.SeekerHistory .AsNoTracking() - .Where(h => h.ArrInstanceId == ic.ArrInstanceId && h.RunId == ic.CurrentRunId) + .Where(h => h.ArrInstanceId == ic.ArrInstanceId && h.CycleId == ic.CurrentCycleId) .MinAsync(h => (DateTime?)h.LastSearchedAt); if (ShouldWaitForMinCycleTime(ic, cycleStartedAt)) diff --git a/code/backend/Cleanuparr.Persistence/DataContext.cs b/code/backend/Cleanuparr.Persistence/DataContext.cs index e6504063..c7a98820 100644 --- a/code/backend/Cleanuparr.Persistence/DataContext.cs +++ b/code/backend/Cleanuparr.Persistence/DataContext.cs @@ -234,7 +234,7 @@ public class DataContext : DbContext .HasForeignKey(s => s.ArrInstanceId) .OnDelete(DeleteBehavior.Cascade); - entity.HasIndex(s => new { s.ArrInstanceId, s.ExternalItemId, s.ItemType, s.SeasonNumber, s.RunId }).IsUnique(); + entity.HasIndex(s => new { s.ArrInstanceId, s.ExternalItemId, s.ItemType, s.SeasonNumber, s.CycleId }).IsUnique(); entity.Property(s => s.LastSearchedAt).HasConversion(new UtcDateTimeConverter()); }); diff --git a/code/backend/Cleanuparr.Persistence/Models/Configuration/Seeker/SeekerInstanceConfig.cs b/code/backend/Cleanuparr.Persistence/Models/Configuration/Seeker/SeekerInstanceConfig.cs index 211d7b0e..4acdd12f 100644 --- a/code/backend/Cleanuparr.Persistence/Models/Configuration/Seeker/SeekerInstanceConfig.cs +++ b/code/backend/Cleanuparr.Persistence/Models/Configuration/Seeker/SeekerInstanceConfig.cs @@ -40,10 +40,10 @@ public sealed record SeekerInstanceConfig public DateTime? LastProcessedAt { get; set; } /// - /// The current cycle run ID. All searches in the same cycle share this ID. + /// The current cycle ID. All searches in the same cycle share this ID. /// When all eligible items have been searched, a new ID is generated to start a fresh cycle. /// - public Guid CurrentRunId { get; set; } = Guid.NewGuid(); + public Guid CurrentCycleId { get; set; } = Guid.NewGuid(); /// /// Total number of eligible items in the library for this instance. diff --git a/code/backend/Cleanuparr.Persistence/Models/Events/AppEvent.cs b/code/backend/Cleanuparr.Persistence/Models/Events/AppEvent.cs index 34bbf855..ca4a0fcc 100644 --- a/code/backend/Cleanuparr.Persistence/Models/Events/AppEvent.cs +++ b/code/backend/Cleanuparr.Persistence/Models/Events/AppEvent.cs @@ -17,7 +17,7 @@ namespace Cleanuparr.Persistence.Models.Events; [Index(nameof(JobRunId))] [Index(nameof(InstanceType))] [Index(nameof(DownloadClientType))] -[Index(nameof(CycleRunId))] +[Index(nameof(CycleId))] public class AppEvent : IEvent { [Key] @@ -86,9 +86,9 @@ public class AppEvent : IEvent public DateTime? CompletedAt { get; set; } /// - /// The Seeker cycle run ID associated with this event (only set for SearchTriggered events) + /// The Seeker cycle ID associated with this event (only set for SearchTriggered events) /// - public Guid? CycleRunId { get; set; } + public Guid? CycleId { get; set; } public bool IsDryRun { get; set; } } \ No newline at end of file diff --git a/code/backend/Cleanuparr.Persistence/Models/State/SeekerHistory.cs b/code/backend/Cleanuparr.Persistence/Models/State/SeekerHistory.cs index d19a2e3c..45f08833 100644 --- a/code/backend/Cleanuparr.Persistence/Models/State/SeekerHistory.cs +++ b/code/backend/Cleanuparr.Persistence/Models/State/SeekerHistory.cs @@ -41,10 +41,10 @@ public sealed record SeekerHistory public int SeasonNumber { get; set; } /// - /// The run ID for cycle-based tracking. All searches in the same cycle share a RunId. - /// When all items have been searched, a new RunId is generated to start a fresh cycle. + /// The cycle ID. All searches in the same cycle share a CycleId. + /// When all items have been searched, a new CycleId is generated to start a fresh cycle. /// - public Guid RunId { get; set; } + public Guid CycleId { get; set; } /// /// When this item was last searched diff --git a/code/frontend/src/app/core/api/search-stats.api.ts b/code/frontend/src/app/core/api/search-stats.api.ts index bd7afcb4..42af4f40 100644 --- a/code/frontend/src/app/core/api/search-stats.api.ts +++ b/code/frontend/src/app/core/api/search-stats.api.ts @@ -25,10 +25,10 @@ export class SearchStatsApi { ); } - getEvents(page = 1, pageSize = 50, instanceId?: string, cycleRunId?: string): Observable> { + getEvents(page = 1, pageSize = 50, instanceId?: string, cycleId?: string): Observable> { const params: Record = { page, pageSize }; if (instanceId) params['instanceId'] = instanceId; - if (cycleRunId) params['cycleRunId'] = cycleRunId; + if (cycleId) params['cycleId'] = cycleId; return this.http.get>('/api/seeker/search-stats/events', { params }); } } diff --git a/code/frontend/src/app/core/models/search-stats.models.ts b/code/frontend/src/app/core/models/search-stats.models.ts index faeae66f..09236643 100644 --- a/code/frontend/src/app/core/models/search-stats.models.ts +++ b/code/frontend/src/app/core/models/search-stats.models.ts @@ -6,7 +6,7 @@ export interface InstanceSearchStat { totalSearchCount: number; lastSearchedAt: string | null; lastProcessedAt: string | null; - currentRunId: string | null; + currentCycleId: string | null; cycleItemsSearched: number; cycleItemsTotal: number; cycleStartedAt: string | null; @@ -51,6 +51,6 @@ export interface SearchEvent { searchStatus: string | null; completedAt: string | null; grabbedItems: unknown[] | null; - cycleRunId: string | null; + cycleId: string | null; isDryRun: boolean; } diff --git a/code/frontend/src/app/features/search-stats/search-stats.component.html b/code/frontend/src/app/features/search-stats/search-stats.component.html index bb9cab69..89b5be5a 100644 --- a/code/frontend/src/app/features/search-stats/search-stats.component.html +++ b/code/frontend/src/app/features/search-stats/search-stats.component.html @@ -84,7 +84,7 @@
- {{ inst.currentRunId ? inst.currentRunId.substring(0, 8) : '—' }} + {{ inst.currentCycleId ? inst.currentCycleId.substring(0, 8) : '—' }} Cycle ID @@ -150,8 +150,8 @@ @if (event.isDryRun) { Dry Run } - @if (event.cycleRunId) { - {{ event.cycleRunId.substring(0, 8) }} + @if (event.cycleId) { + {{ event.cycleId.substring(0, 8) }} } {{ event.instanceName }} {{ event.timestamp | date:'yyyy-MM-dd HH:mm' }} @@ -251,8 +251,8 @@ @if (event.isDryRun) { Dry Run } - @if (event.cycleRunId) { - {{ event.cycleRunId.substring(0, 8) }} + @if (event.cycleId) { + {{ event.cycleId.substring(0, 8) }} } {{ event.timestamp | date:'yyyy-MM-dd HH:mm' }}
diff --git a/code/frontend/src/app/features/search-stats/search-stats.component.ts b/code/frontend/src/app/features/search-stats/search-stats.component.ts index 9cf24327..8eefe57b 100644 --- a/code/frontend/src/app/features/search-stats/search-stats.component.ts +++ b/code/frontend/src/app/features/search-stats/search-stats.component.ts @@ -256,14 +256,14 @@ export class SearchStatsComponent implements OnInit { private loadEvents(): void { this.loading.set(true); const instanceId = this.selectedInstanceId() || undefined; - let cycleRunId: string | undefined; + let cycleId: string | undefined; if (this.cycleFilter() === 'current' && instanceId) { const instance = this.summary()?.perInstanceStats.find(s => s.instanceId === instanceId); - cycleRunId = instance?.currentRunId ?? undefined; + cycleId = instance?.currentCycleId ?? undefined; } - this.api.getEvents(this.eventsPage(), this.pageSize(), instanceId, cycleRunId).subscribe({ + this.api.getEvents(this.eventsPage(), this.pageSize(), instanceId, cycleId).subscribe({ next: (result) => { this.events.set(result.items); this.eventsTotalRecords.set(result.totalCount);