fixed cycle id naming inconsistency

This commit is contained in:
Flaminel
2026-03-24 22:06:21 +02:00
parent bd28123bb1
commit 45cb384fc7
16 changed files with 87 additions and 87 deletions

View File

@@ -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; }

View File

@@ -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; }
}

View File

@@ -72,10 +72,10 @@ public sealed class SearchStatsController : ControllerBase
.ToListAsync();
// Count items searched in current cycle per instance
List<Guid> currentRunIds = instanceConfigs.Select(ic => ic.CurrentRunId).ToList();
List<Guid> 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;

View File

@@ -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]

View File

@@ -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<HashSet<SearchItem>>()),
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)

View File

@@ -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.
/// </summary>
public async Task<Guid> PublishSearchTriggered(string instanceName, int itemCount, IEnumerable<string> items, SeekerSearchType searchType, Guid? cycleRunId = null)
public async Task<Guid> PublishSearchTriggered(string instanceName, int itemCount, IEnumerable<string> 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();

View File

@@ -20,7 +20,7 @@ public interface IEventPublisher
Task PublishSearchNotTriggered(string hash, string itemName);
Task<Guid> PublishSearchTriggered(string instanceName, int itemCount, IEnumerable<string> items, SeekerSearchType searchType, Guid? cycleRunId = null);
Task<Guid> PublishSearchTriggered(string instanceName, int itemCount, IEnumerable<string> items, SeekerSearchType searchType, Guid? cycleId = null);
Task PublishSearchCompleted(Guid eventId, SearchCommandStatus status, object? resultData = null);
}

View File

@@ -315,7 +315,7 @@ public sealed class Seeker : IHandler
// Load search history for the current cycle
List<SeekerHistory> 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<long> 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<long> searchedIds,
List<string>? 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.
/// </summary>
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))

View File

@@ -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());
});

View File

@@ -40,10 +40,10 @@ public sealed record SeekerInstanceConfig
public DateTime? LastProcessedAt { get; set; }
/// <summary>
/// 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.
/// </summary>
public Guid CurrentRunId { get; set; } = Guid.NewGuid();
public Guid CurrentCycleId { get; set; } = Guid.NewGuid();
/// <summary>
/// Total number of eligible items in the library for this instance.

View File

@@ -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; }
/// <summary>
/// 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)
/// </summary>
public Guid? CycleRunId { get; set; }
public Guid? CycleId { get; set; }
public bool IsDryRun { get; set; }
}

View File

@@ -41,10 +41,10 @@ public sealed record SeekerHistory
public int SeasonNumber { get; set; }
/// <summary>
/// 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.
/// </summary>
public Guid RunId { get; set; }
public Guid CycleId { get; set; }
/// <summary>
/// When this item was last searched

View File

@@ -25,10 +25,10 @@ export class SearchStatsApi {
);
}
getEvents(page = 1, pageSize = 50, instanceId?: string, cycleRunId?: string): Observable<PaginatedResult<SearchEvent>> {
getEvents(page = 1, pageSize = 50, instanceId?: string, cycleId?: string): Observable<PaginatedResult<SearchEvent>> {
const params: Record<string, string | number> = { page, pageSize };
if (instanceId) params['instanceId'] = instanceId;
if (cycleRunId) params['cycleRunId'] = cycleRunId;
if (cycleId) params['cycleId'] = cycleId;
return this.http.get<PaginatedResult<SearchEvent>>('/api/seeker/search-stats/events', { params });
}
}

View File

@@ -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;
}

View File

@@ -84,7 +84,7 @@
</div>
<div class="instance-card__stat">
<span class="instance-card__stat-value instance-card__stat-value--small">
{{ inst.currentRunId ? inst.currentRunId.substring(0, 8) : '—' }}
{{ inst.currentCycleId ? inst.currentCycleId.substring(0, 8) : '—' }}
</span>
<app-tooltip text="Unique identifier for the current search cycle. Changes when all items have been searched">
<span class="instance-card__stat-label">Cycle ID</span>
@@ -150,8 +150,8 @@
@if (event.isDryRun) {
<app-badge severity="accent" size="sm">Dry Run</app-badge>
}
@if (event.cycleRunId) {
<span class="list-row__cycle">{{ event.cycleRunId.substring(0, 8) }}</span>
@if (event.cycleId) {
<span class="list-row__cycle">{{ event.cycleId.substring(0, 8) }}</span>
}
<span class="list-row__meta">{{ event.instanceName }}</span>
<span class="list-row__time">{{ event.timestamp | date:'yyyy-MM-dd HH:mm' }}</span>
@@ -251,8 +251,8 @@
@if (event.isDryRun) {
<app-badge severity="accent" size="sm">Dry Run</app-badge>
}
@if (event.cycleRunId) {
<span class="list-row__cycle">{{ event.cycleRunId.substring(0, 8) }}</span>
@if (event.cycleId) {
<span class="list-row__cycle">{{ event.cycleId.substring(0, 8) }}</span>
}
<span class="list-row__time">{{ event.timestamp | date:'yyyy-MM-dd HH:mm' }}</span>
</div>

View File

@@ -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);