diff --git a/code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Responses/InstanceFunnelStat.cs b/code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Responses/InstanceFunnelStat.cs
new file mode 100644
index 00000000..c5d71533
--- /dev/null
+++ b/code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Responses/InstanceFunnelStat.cs
@@ -0,0 +1,40 @@
+using Cleanuparr.Persistence.Models.State;
+
+namespace Cleanuparr.Api.Features.Seeker.Contracts.Responses;
+
+///
+/// How many items and units survived each Seeker stage on the cycle's latest run.
+/// Items count movies or series, units count searches.
+///
+public sealed record InstanceFunnelStat
+{
+ public int LibraryItems { get; init; }
+ public int ReleasedItems { get; init; }
+ public int MonitoredItems { get; init; }
+ public int TagFilteredItems { get; init; }
+ public int EligibleUnits { get; init; }
+ public int QueuedUnits { get; init; }
+ public int RecentlyGrabbedUnits { get; init; }
+ public int SearchedUnits { get; init; }
+ public int RemainingUnits { get; init; }
+ public DateTimeOffset CountedAt { get; init; }
+
+ ///
+ /// Null until a run counted the cycle's funnel.
+ ///
+ public static InstanceFunnelStat? From(SeekerCycle? cycle) => cycle?.FunnelCountedAt is not { } countedAt
+ ? null
+ : new InstanceFunnelStat
+ {
+ LibraryItems = cycle.LibraryItems,
+ ReleasedItems = cycle.ReleasedItems,
+ MonitoredItems = cycle.MonitoredItems,
+ TagFilteredItems = cycle.TagFilteredItems,
+ EligibleUnits = cycle.EligibleNow,
+ QueuedUnits = cycle.QueuedUnits,
+ RecentlyGrabbedUnits = cycle.RecentlyGrabbedUnits,
+ SearchedUnits = Math.Max(0, cycle.EligibleNow - cycle.QueuedUnits - cycle.RecentlyGrabbedUnits - cycle.RemainingUnits),
+ RemainingUnits = cycle.RemainingUnits,
+ CountedAt = countedAt,
+ };
+}
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 ad79bb26..8accacfb 100644
--- a/code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Responses/InstanceSearchStat.cs
+++ b/code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Responses/InstanceSearchStat.cs
@@ -17,4 +17,5 @@ public sealed record InstanceSearchStat
public DateTimeOffset? CycleCompletedAt { get; init; }
public DateTimeOffset? LastCycleStartedAt { get; init; }
public DateTimeOffset? LastCycleCompletedAt { get; init; }
+ public InstanceFunnelStat? Funnel { 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 82d7e4ae..684c1a4a 100644
--- a/code/backend/Cleanuparr.Api/Features/Seeker/Controllers/SearchStatsController.cs
+++ b/code/backend/Cleanuparr.Api/Features/Seeker/Controllers/SearchStatsController.cs
@@ -111,6 +111,7 @@ public sealed class SearchStatsController : ControllerBase
CycleCompletedAt = current?.CompletedAt,
LastCycleStartedAt = last?.StartedAt,
LastCycleCompletedAt = last?.CompletedAt,
+ Funnel = InstanceFunnelStat.From(current),
};
}).ToList();