Skip to content

Add sorting and filters for Seeker stats#576

Merged
Flaminel merged 47 commits into
mainfrom
add_seeker_filters
Apr 25, 2026
Merged

Add sorting and filters for Seeker stats#576
Flaminel merged 47 commits into
mainfrom
add_seeker_filters

Conversation

@Flaminel

Copy link
Copy Markdown
Contributor

Relates to #568

@Flaminel

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 21, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Flaminel

Copy link
Copy Markdown
Contributor Author

@greptileai review

@Flaminel Flaminel requested a review from Copilot April 21, 2026 08:25
@codecov

codecov Bot commented Apr 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.94220% with 52 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
.../Seeker/Controllers/CustomFormatScoreController.cs 67.40% 35 Missing and 9 partials ⚠️
...atures/Seeker/Controllers/SearchStatsController.cs 75.00% 6 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds table-style sorting and advanced filtering to Seeker Stats (Searches / Quality Scores / Upgrades), plus supporting UI primitives (drawer, sticky-aware headers) and backend query capabilities (new sort/filter params and a new LastUpgradedAt field).

Changes:

  • Introduces a reusable filter drawer UI and sticky-header “stuck” styling hook (directive + SCSS updates).
  • Adds sort + search + advanced filters to Seeker Stats tabs, backed by updated frontend API clients.
  • Extends backend Seeker endpoints to support filtering/sorting and persists LastUpgradedAt for CF score entries (incl. migration/backfill).

Reviewed changes

Copilot reviewed 46 out of 47 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
code/frontend/src/styles/_page-animations.scss New mixin for consistent staggered page-entry animations.
code/frontend/src/styles/_data-toolbar.scss Updates sticky toolbar behavior to apply frosting only when “stuck” + transitions.
code/frontend/src/styles/_animations.scss Tweaks slide-up animation keyframes.
code/frontend/src/app/ui/index.ts Exposes new DrawerComponent from the UI barrel.
code/frontend/src/app/ui/drawer/drawer.component.ts New drawer component (visibility model, escape/backdrop close, focus-first control).
code/frontend/src/app/ui/drawer/drawer.component.scss Drawer/backdrop styling + responsive layout.
code/frontend/src/app/ui/drawer/drawer.component.html Drawer structure + optional header/footer content projection.
code/frontend/src/app/features/strikes/strikes.component.ts Wires in StickyAwareDirective for toolbar stuck-state styling.
code/frontend/src/app/features/strikes/strikes.component.scss Switches to shared page stagger animation utilities.
code/frontend/src/app/features/strikes/strikes.component.html Adds stickyAware to the toolbar element.
code/frontend/src/app/features/seeker-stats/upgrades-tab/upgrades-tab.component.ts Adds search + sort + advanced filters drawer; updates API usage to query-object form.
code/frontend/src/app/features/seeker-stats/upgrades-tab/upgrades-tab.component.scss Adds sticky toolbar + shared stagger animations + filter drawer/table styles.
code/frontend/src/app/features/seeker-stats/upgrades-tab/upgrades-tab.component.html Adds search/sort controls and a drawer-based filter UI.
code/frontend/src/app/features/seeker-stats/seeker-stats.component.scss Applies shared stagger animation; disables animation on tab content to avoid backdrop-filter clipping.
code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.ts Adds sort + advanced filters drawer; updates events query call to use new API query object.
code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.scss Adds sticky toolbar + shared stagger animations + drawer/chip styling.
code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.html Adds sort controls and a drawer for cycle/status/type/reason/grabbed filters.
code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.ts Adds sorting, quality-profile/cutoff/monitored filters via drawer; updates API call to query-object form.
code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.scss Adds sticky toolbar + shared stagger animations + drawer/table/detail styles.
code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.html Adds sort controls, last-upgraded display, and filter drawer UI.
code/frontend/src/app/features/logs-component/logs.component.ts Wires in StickyAwareDirective for toolbar stuck-state styling.
code/frontend/src/app/features/logs-component/logs.component.scss Switches to shared page stagger animation utilities.
code/frontend/src/app/features/logs-component/logs.component.html Adds stickyAware to the toolbar element.
code/frontend/src/app/features/events/events.component.ts Wires in StickyAwareDirective for toolbar stuck-state styling.
code/frontend/src/app/features/events/events.component.scss Switches to shared page stagger animation utilities.
code/frontend/src/app/features/events/events.component.html Adds stickyAware to the toolbar element.
code/frontend/src/app/features/dashboard/dashboard.component.ts Updates recent upgrades call to new query-object API signature.
code/frontend/src/app/features/dashboard/dashboard.component.scss Uses shared stagger animation utility for dashboard rows.
code/frontend/src/app/core/directives/sticky-aware.directive.ts New directive toggling .is-stuck based on sticky intersection state.
code/frontend/src/app/core/api/search-stats.api.ts Adds typed query object + sorting/filtering params and HttpParams construction.
code/frontend/src/app/core/api/cf-score.api.ts Adds typed query objects + enums for sorting/filtering; updates endpoints to accept richer params.
code/backend/Cleanuparr.Persistence/Models/State/CustomFormatScoreEntry.cs Adds LastUpgradedAt column to persisted CF score entries.
code/backend/Cleanuparr.Persistence/Migrations/Data/DataContextModelSnapshot.cs EF snapshot updated for LastUpgradedAt + index.
code/backend/Cleanuparr.Persistence/Migrations/Data/20260420134250_AddLastUpgradedAtToCfScoreEntry.cs Migration adds last_upgraded_at, index, and backfills from history via SQL.
code/backend/Cleanuparr.Persistence/Migrations/Data/20260420134250_AddLastUpgradedAtToCfScoreEntry.Designer.cs EF migration designer output for the new schema.
code/backend/Cleanuparr.Persistence/DataContext.cs Adds index + UTC conversion for LastUpgradedAt.
code/backend/Cleanuparr.Infrastructure/Features/Jobs/CustomFormatScoreSyncer.cs Sets LastUpgradedAt when a score increase is detected during sync.
code/backend/Cleanuparr.Domain/Enums/SortDirection.cs New shared sort-direction enum for API query params.
code/backend/Cleanuparr.Domain/Enums/SearchEventsSortBy.cs New sort key enum for search events.
code/backend/Cleanuparr.Domain/Enums/MonitoredFilter.cs New monitored-state filter enum for CF scores.
code/backend/Cleanuparr.Domain/Enums/CutoffFilter.cs New cutoff filter enum for CF scores.
code/backend/Cleanuparr.Domain/Enums/CfUpgradesSortBy.cs New sort key enum for CF upgrades.
code/backend/Cleanuparr.Domain/Enums/CfScoresSortBy.cs New sort key enum for CF scores.
code/backend/Cleanuparr.Api/Features/Seeker/Controllers/SearchStatsController.cs Adds query params + filtering + sorting for search events endpoint.
code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs Adds query params + filters + sorting for scores and upgrades endpoints; extends instances response.
code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Responses/CustomFormatScoreEntryResponse.cs Includes LastUpgradedAt in API response contract.
code/backend/Cleanuparr.Api.Tests/Features/Seeker/CustomFormatScoreControllerTests.cs Updates tests to use new filter enums and sort enum.
Files not reviewed (1)
  • code/backend/Cleanuparr.Persistence/Migrations/Data/20260420134250_AddLastUpgradedAtToCfScoreEntry.Designer.cs: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread code/frontend/src/app/ui/drawer/drawer.component.html
Comment thread code/frontend/src/styles/_data-toolbar.scss Outdated
@greptile-apps

greptile-apps Bot commented Apr 21, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds server-side sorting, filtering, and pagination to the Seeker stats tabs (CF score quality, upgrades, and search events). The most significant backend change replaces the previous in-memory upgrade-detection loop in GetRecentUpgrades with a SQL CTE using LAG(), which correctly handles upgrades that cross a window boundary and moves sort/pagination to the database layer. A new last_upgraded_at column is added to custom_format_score_entries with a backfill migration and a matching index.

Confidence Score: 5/5

Safe to merge; only P2 findings remain (stale doc comment and a defensive-coding suggestion).

All remaining findings are P2: a doc-comment typo on pageSize range and a non-defensive Enum.Parse that is safe in practice since values come from EF Core writes. The previously flagged full-scan concern is directly addressed by the new SQL CTE approach.

SearchStatsController.cs (doc comment) and CustomFormatScoreController.cs (Enum.Parse).

Important Files Changed

Filename Overview
code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs Refactored GetRecentUpgrades to use a SQL CTE with LAG() for upgrade detection + server-side sort/pagination; GetCustomFormatScores gains rich LINQ-based filters; minor: Enum.Parse without try-catch on ItemType.
code/backend/Cleanuparr.Api/Features/Seeker/Controllers/SearchStatsController.cs New GetEvents endpoint with comprehensive filtering/sorting; pageSize doc comment says [1,100] but code clamps to 500.
code/backend/Cleanuparr.Persistence/Migrations/Data/20260420134250_AddLastUpgradedAtToCfScoreEntry.cs Adds nullable last_upgraded_at column with LAG()-based backfill and an index; Up/Down symmetric.
code/backend/Cleanuparr.Infrastructure/Features/Jobs/CustomFormatScoreSyncer.cs UpsertCustomFormatScore now sets LastUpgradedAt when score strictly increases; logic is correct and symmetric with the CTE approach in the controller.
code/frontend/src/app/features/seeker-stats/upgrades-tab/upgrades-tab.component.ts New upgrades tab with load-token anti-flicker pattern, drawer-based advanced filters, and sort/pagination; follows existing Angular patterns.
code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.ts Searches tab adds drawer-based advanced filters (status multi-select, type, reason, grabbed tri-state) with cycle-to-all fallback when no instance is selected.
code/backend/Cleanuparr.Api.Tests/Features/Seeker/CustomFormatScoreControllerTests.cs Test suite updated for new filter/sort enum params; new tests for CutoffFilter.Met, Unmonitored, quality profile, and explicit sort direction cover the main code paths.

Sequence Diagram

sequenceDiagram
    participant FE as Frontend Tab
    participant API as Controller
    participant DB as SQLite (DataContext)

    Note over FE,DB: CF Score Upgrades (new SQL path)
    FE->>API: GET /api/seeker/cf-scores/upgrades?page=1&sortBy=UpgradedAt&days=30
    API->>DB: CTE: WITH scored (LAG over full history), upgrades WHERE score > prev_score
    DB-->>API: UpgradeSqlRow[]
    API->>DB: CTE + SELECT COUNT(*) FROM upgrades WHERE filters
    DB-->>API: totalCount
    API-->>FE: { Items, Page, TotalCount, TotalPages }

    Note over FE,DB: CF Score Entries (LINQ path)
    FE->>API: GET /api/seeker/cf-scores?cutoffFilter=Below&sortBy=CurrentScore
    API->>DB: EF Core query with Where+OrderBy+Skip+Take
    DB-->>API: CustomFormatScoreEntry[]
    API-->>FE: { Items, Page, TotalCount, TotalPages }

    Note over FE,DB: Search Events
    FE->>API: GET /api/seeker/search-stats/events?sortBy=Timestamp&searchStatus=Completed
    API->>DB: EventsContext LINQ with filters + ThenByDescending(Timestamp)
    DB-->>API: AppEvent[] (page)
    API->>DB: ArrInstances lookup for InstanceType resolution
    DB-->>API: instanceTypeMap
    API-->>FE: { Items, Page, TotalCount, TotalPages }
Loading

Reviews (2): Last reviewed commit: "added tests for new seeker sort and filt..." | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
code/backend/Cleanuparr.Api/Features/Seeker/Controllers/SearchStatsController.cs (1)

120-135: ⚠️ Potential issue | 🟡 Minor

Document the expanded query contract.

GetEvents now exposes a much larger public query surface, but the XML docs still only give a one-line summary. Please add <param> docs for the new sort and filter inputs, plus the default ordering behavior.

As per coding guidelines "Add XML documentation comments for public APIs in C#".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@code/backend/Cleanuparr.Api/Features/Seeker/Controllers/SearchStatsController.cs`
around lines 120 - 135, Update the XML docs on the public GetEvents action to
fully document the expanded query contract: expand the one-line <summary> into a
brief description of what the endpoint returns and add <param> entries for page,
pageSize, instanceId, cycleId, search, sortBy, sortDirection, searchStatus,
searchType, searchReason and grabbed describing each parameter's purpose and
accepted values where applicable; explicitly document default values (page=1,
pageSize=50) and the default ordering behavior (sortBy defaults to
SearchEventsSortBy.Timestamp and sortDirection defaults to Desc) so consumers
know how results are filtered and ordered.
code/frontend/src/app/features/seeker-stats/upgrades-tab/upgrades-tab.component.ts (1)

199-223: ⚠️ Potential issue | 🟠 Major

Prevent stale responses from overwriting newer filter state.

Each interaction fires a fresh request, but older in-flight responses still update the same signals when they finish. If a slower previous request resolves last, the table can end up showing rows/counts for the wrong sort or filter selection.

💡 One lightweight guard
 export class UpgradesTabComponent implements OnInit {
   private static readonly PAGE_SIZE_KEY = 'cleanuparr-page-size-seeker-upgrades';
+  private latestLoadToken = 0;
@@
   private loadUpgrades(): void {
     this.loading.set(true);
+    const loadToken = ++this.latestLoadToken;
     const a = this.applied();
     const days = parseInt(a.timeRange, 10);
     const instanceId = this.selectedInstanceId() || undefined;
@@
     }).subscribe({
       next: (result) => {
+        if (loadToken !== this.latestLoadToken) return;
         this.upgrades.set(result.items);
         this.totalRecords.set(result.totalCount);
         this.loading.set(false);
       },
       error: () => {
+        if (loadToken !== this.latestLoadToken) return;
         this.loading.set(false);
         this.toast.error('Failed to load upgrades');
       },
     });
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@code/frontend/src/app/features/seeker-stats/upgrades-tab/upgrades-tab.component.ts`
around lines 199 - 223, The loadUpgrades method can let out-of-order responses
overwrite newer state; add a lightweight request guard by stamping each call
with a monotonic requestId (e.g., increment a private this._latestRequestId
before calling this.api.getRecentUpgrades) and capture that id in the subscribe
callbacks for next and error; inside next/error only apply this.upgrades.set,
this.totalRecords.set, this.loading.set, and this.toast.error if the captured id
equals this._latestRequestId, otherwise ignore the response. Update the
loadUpgrades method and declare the private this._latestRequestId field used by
the guard.
code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs (1)

288-318: ⚠️ Potential issue | 🟠 Major

This endpoint materializes far more rows than it needs.

ToListAsync() happens before any deduplication, so the instances endpoint loads one row per tracked score entry just to build filter options. On large libraries that makes this action O(all score rows), and the later Distinct() is still case-sensitive, so mixed-case profile names can appear twice. Push the distinct projection into SQL first, then collapse profile names with StringComparer.OrdinalIgnoreCase.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs`
around lines 288 - 318, The code fetches all score-entry rows into raw via
ToListAsync() before deduplicating, and Distinct() is case-sensitive; change the
LINQ to perform the distinct projection in SQL before materialization: move the
.Select(...) projection (projecting ArrInstanceId/Name/ItemType and a
case-normalized QualityProfileName, e.g. e.QualityProfileName.ToLower() or
ToUpper()) immediately before calling ToListAsync() so the database returns one
row per distinct profile name per instance, then perform the in-memory grouping
(the instances variable) using StringComparer.OrdinalIgnoreCase to collapse any
remaining case variants; look for _dataContext.CustomFormatScoreEntries, the
anonymous projection that creates Id/Name/ItemType/QualityProfileName, the
ToListAsync() call on raw, and the later Distinct()/OrderBy usage to apply this
change.
🧹 Nitpick comments (4)
code/frontend/src/app/core/api/search-stats.api.ts (1)

19-30: Type the remaining enum-backed filters end-to-end.

searchStatus, searchType, and searchReason are still plain strings, so callers can build invalid requests without any compile-time signal. Prefer enum or string-literal types here as well to keep the frontend contract aligned with the backend.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/frontend/src/app/core/api/search-stats.api.ts` around lines 19 - 30,
SearchEventsQuery currently types searchStatus, searchType, and searchReason as
plain string arrays; update them to enum/string-literal types to match backend
contracts and provide compile-time safety. Replace searchStatus?: string[] with
the appropriate enum/union type (e.g., SearchStatus[]), searchType?: string[]
with SearchType[] and searchReason?: string[] with SearchReason[] (or the
existing string-literal unions), import or reference the existing enums/types
used elsewhere in the codebase, and update any callers/tests to use the enum
values instead of raw strings.
code/frontend/src/styles/_page-animations.scss (1)

9-19: Consider reduced-motion support in the shared stagger mixin.

Since this utility is now reused broadly, adding a prefers-reduced-motion branch here avoids repeated per-page overrides.

♻️ Proposed enhancement
 `@mixin` page-section-stagger($increment: 40ms, $start: 0ms, $count: 10) {
   > * {
     animation: slide-up var(--duration-normal) var(--ease-default) both;
   }

   `@for` $i from 1 through $count {
     > :nth-child(#{$i}) {
       animation-delay: #{$start + ($i - 1) * $increment};
     }
   }
+
+  `@media` (prefers-reduced-motion: reduce) {
+    > * {
+      animation: none;
+    }
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/frontend/src/styles/_page-animations.scss` around lines 9 - 19, The
shared mixin page-section-stagger should respect users' reduced-motion
preference: inside the mixin (mixin page-section-stagger) add a
prefers-reduced-motion: reduce branch that disables animations for the children
(e.g., set animation: none and remove animation-delay/transition behavior or set
transition-duration/animation-duration to 0) so pages using this mixin don't
need per-page overrides; ensure both the > * rule (animation: slide-up ...) and
the per-child animation-delay rules are overridden inside the `@media`
(prefers-reduced-motion: reduce) block to fully disable the staggered animation.
code/backend/Cleanuparr.Persistence/Migrations/Data/20260420134250_AddLastUpgradedAtToCfScoreEntry.cs (1)

20-23: Create the new index after the backfill.

Right now the migration builds ix_custom_format_score_entries_last_upgraded_at before the bulk UPDATE, so every populated row also updates the index. On larger seeker datasets that can make startup migrations much slower than necessary.

💡 Proposed fix
-            migrationBuilder.CreateIndex(
-                name: "ix_custom_format_score_entries_last_upgraded_at",
-                table: "custom_format_score_entries",
-                column: "last_upgraded_at");
-
             // Backfill last_upgraded_at from existing history: per item, the most recent
             // recorded_at at which the score strictly exceeded the preceding score.
             migrationBuilder.Sql(@"
@@
 UPDATE custom_format_score_entries
 SET last_upgraded_at = (
     SELECT last_upgraded_at FROM upgrades u
     WHERE u.arr_instance_id = custom_format_score_entries.arr_instance_id
       AND u.external_item_id = custom_format_score_entries.external_item_id
       AND u.episode_id = custom_format_score_entries.episode_id
 );
 ");
+
+            migrationBuilder.CreateIndex(
+                name: "ix_custom_format_score_entries_last_upgraded_at",
+                table: "custom_format_score_entries",
+                column: "last_upgraded_at");

Also applies to: 27-58

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@code/backend/Cleanuparr.Persistence/Migrations/Data/20260420134250_AddLastUpgradedAtToCfScoreEntry.cs`
around lines 20 - 23, The migration currently calls migrationBuilder.CreateIndex
for "ix_custom_format_score_entries_last_upgraded_at" on table
"custom_format_score_entries" before performing the bulk backfill UPDATE (see
the backfill block around lines 27-58); move the CreateIndex call so it runs
after the backfill completes (or instead drop/recreate the index after the
UPDATE), ensuring the index creation occurs only once after rows are populated
to avoid per-row index updates during the backfill.
code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs (1)

288-320: Add XML docs for the updated public endpoint.

GetInstances now returns a richer payload, but this action still has no XML documentation like the other public endpoints in this controller.

As per coding guidelines "Add XML documentation comments for public APIs in C#".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs`
around lines 288 - 320, Add XML documentation comments to the public action that
returns instances (the GetInstances endpoint in CustomFormatScoreController)
describing purpose, parameters (if any), and the response shape; include a
<summary> that explains the richer payload, a <returns> describing that it
returns an OkObjectResult with an Instances collection containing Id, Name,
ItemType and QualityProfiles, and any <remarks> or <response> tags required by
project conventions so the public API aligns with other documented endpoints in
this controller.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Responses/CustomFormatScoreEntryResponse.cs`:
- Line 20: Add an XML documentation comment for the public property
LastUpgradedAt on the CustomFormatScoreEntryResponse record to describe its
purpose and semantics; include a <summary> that explains this nullable DateTime
indicates when the entry was last upgraded (or null if never upgraded) and, if
relevant, a <remarks> or <value> note about time zone or format expectations so
API consumers understand the contract.

In
`@code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs`:
- Around line 195-198: The current code applies the days/RecordedAt filter to
the IQueryable (variable query) before computing upgrades, which removes the
prior-row baseline needed to detect improvements at the window edge; modify the
flow so you first compute per-item upgrades (use the full time series or include
the last pre-window row as baseline), populate an UpgradedAt timestamp or flag
for each item, and only then apply the days filter (i.e., filter by UpgradedAt
or by the computed upgrade flag) instead of trimming by RecordedAt up front;
locate the logic in CustomFormatScoreController where query is built and
adjusted, and change the order so upgrade detection (and any projection that
produces UpgradedAt) happens before applying the days filter.

In `@code/backend/Cleanuparr.Domain/Enums/CfScoresSortBy.cs`:
- Around line 6-14: Public enum CfScoresSortBy and its members (Title,
CurrentScore, CutoffScore, QualityProfile, LastSyncedAt, LastUpgradedAt) are
missing XML documentation; add XML summary comments above the enum declaration
and above each enum member describing the purpose of the enum and what each sort
key represents (e.g., "Sort by title", "Sort by current score", etc.), following
the project's XML doc style so the public API is documented for consumers and
tooling.

In `@code/backend/Cleanuparr.Domain/Enums/CfUpgradesSortBy.cs`:
- Around line 5-14: Add XML documentation comments to the public enum
CfUpgradesSortBy and each of its values (UpgradedAt, Title, NewScore,
PreviousScore, ScoreDelta, CutoffScore): add a summary for the enum describing
its purpose (used to specify the sort order for CF upgrades) and add brief
<summary> lines for each member explaining what field they sort by (e.g.,
UpgradedAt = sort by upgrade timestamp, Title = sort by item title, NewScore =
sort by new score after upgrade, PreviousScore = sort by score before upgrade,
ScoreDelta = sort by change in score, CutoffScore = sort by cutoff threshold).
Ensure the XML comments follow standard C# doc format (///
<summary>...</summary>) placed immediately above the enum and each enum member.

In `@code/backend/Cleanuparr.Domain/Enums/CutoffFilter.cs`:
- Around line 6-11: Add XML documentation comments to the public enum
CutoffFilter and each of its members (All, Below, Met): describe the enum's
purpose and the semantic meaning of each value so the public API contract is
clear; update the declaration for CutoffFilter and add ///
<summary>...</summary> comments for All, Below, and Met following the project's
XML doc style and conventions.

In `@code/backend/Cleanuparr.Domain/Enums/MonitoredFilter.cs`:
- Around line 6-11: Add XML documentation for the public enum MonitoredFilter
and each of its members (All, Monitored, Unmonitored): add a summary on the enum
describing its purpose (e.g., indicates which items are monitored) and add
individual <summary> comments for All, Monitored, and Unmonitored explaining
what each value represents; ensure the comments follow C# XML doc style so the
public API is self-describing and satisfies the coding guideline.

In `@code/backend/Cleanuparr.Domain/Enums/SearchEventsSortBy.cs`:
- Around line 5-12: Add XML documentation comments for the public enum
SearchEventsSortBy and each of its members (Timestamp, Title, Status, Type): add
a /// <summary> for the enum describing its purpose in sorting search event
results and individual /// <summary> comments for each member explaining what
field or behavior they represent when used for sorting; keep the JsonConverter
attribute intact and ensure the comments are placed immediately above the enum
and each enum member so generated API docs include meaningful descriptions.

In `@code/backend/Cleanuparr.Domain/Enums/SortDirection.cs`:
- Around line 5-10: Add XML documentation comments to the public enum
SortDirection and its members (SortDirection, Asc, Desc) so the public contract
has XML docs; update the SortDirection declaration (the enum marked with
[JsonConverter(typeof(JsonStringEnumConverter))]) to include a summary
describing what the enum represents, and add <summary> comments for each member
(Asc — ascending sort order, Desc — descending sort order), following the
project's XML doc style.

In
`@code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.scss`:
- Around line 289-293: Replace the deprecated "word-break: break-word" in
quality-tab.component.scss by setting "word-break: normal" and adding
"overflow-wrap: anywhere" (or "overflow-wrap: break-word" if you prefer legacy
behavior) for the &__text rule and the other similar rule later in the file;
locate the &__text selector in the quality-tab component and update its
declarations to use overflow-wrap and normal word-break to satisfy Stylelint.

In
`@code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.html`:
- Around line 223-228: The status-chip button currently conveys selection only
via CSS; update the template for the button in searches-tab.component.html to
expose that state to assistive tech by adding an aria-pressed binding that uses
the same predicate as the visual state (isStatusDrafted(opt.value)); keep the
existing [class.chip--active]="isStatusDrafted(opt.value)" and
(click)="toggleStatus(opt.value)" intact and bind aria-pressed to
isStatusDrafted(opt.value) so screen readers can detect pressed/unpressed state.

In
`@code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.scss`:
- Around line 280-284: The rule in the searches-tab.component.scss selector
&__value uses the deprecated property "word-break: break-word"; replace that
with a non-deprecated wrapping rule by removing "word-break: break-word" and
adding "overflow-wrap: anywhere;" (or "overflow-wrap: break-word;" if you prefer
less aggressive breaking) so the selector still breaks long words without
Stylelint errors.

In
`@code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.ts`:
- Around line 341-347: The code only derives cycleId when instanceId is present,
so when a.cycleFilter === 'current' but instanceId is cleared the request ends
up unfiltered; update the logic in searches-tab.component.ts around the
applied()/cycleId block so that if a.cycleFilter === 'current' and instanceId is
falsy you normalize the filter before querying—either convert a.cycleFilter to
'all' (or another explicit fallback) or explicitly set cycleId to undefined and
update the applied filter object; locate the applied() result, the cycleFilter
variable, instanceId, summary()?.perInstanceStats and currentCycleId usage and
ensure the request uses the normalized cycleFilter/cycleId consistently.

In `@code/frontend/src/app/features/seeker-stats/seeker-stats.component.scss`:
- Around line 12-15: In seeker-stats.component.scss add a single blank line
immediately before the inline comment that begins "// Chromium retains the
compositor layer..." so the comment block is separated from the prior rule;
ensure the comment stays directly above the "animation: none;" declaration and
that there is an empty line above the comment to satisfy stylelint spacing
rules.

In
`@code/frontend/src/app/features/seeker-stats/upgrades-tab/upgrades-tab.component.scss`:
- Around line 168-172: In the &__text rule (selector &__text in
upgrades-tab.component.scss) replace the deprecated declaration "word-break:
break-word" with the modern equivalent "overflow-wrap: anywhere" (remove the
deprecated property and add overflow-wrap: anywhere so long words will break
correctly while preserving the existing font-weight and color).

In `@code/frontend/src/app/ui/drawer/drawer.component.html`:
- Around line 6-13: Make the aside's aria-labelledby conditional and use a
per-instance unique id for the title to avoid missing references and ID
collisions: generate a unique string (e.g., drawerTitleId) in the
DrawerComponent class, bind the H2's id to that value when title() is truthy
(id="{{drawerTitleId}}" or the framework binding equivalent), and set the
aside's aria-labelledby only when title() exists (otherwise remove it and
require/provide an aria-label fallback input). Update template bindings
referencing title() and the header H2 (drawer__title) and keep the close()
button unchanged.

In `@code/frontend/src/app/ui/drawer/drawer.component.scss`:
- Around line 19-21: The .drawer rule violates stylelint because there is no
blank line between the mixin include and the first declaration; update the
.drawer block (the `@include` glass('elevated') line and the position: relative
declaration) to ensure there is a single blank line separating the mixin include
and the first CSS declaration so stylelint passes.

In `@code/frontend/src/app/ui/drawer/drawer.component.ts`:
- Around line 19-60: When the drawer opens save the currently focused element
and add Angular CDK focus-trap to the drawer element, then restore focus on
close/destroy: in the effect inside the constructor (where you detect
this.visible()) capture document.activeElement (store e.g. this.previousFocus)
before calling focusFirstControl(); add cdkTrapFocus to the drawer root element
in the component template so Tab/Shift+Tab is trapped; update close() to restore
focus to this.previousFocus (if it’s focusable and still in the document) before
emitting closed and setting visible false; also restore focus in ngOnDestroy()
to cover unmounts; keep using focusFirstControl() to pick the initial focus
target.

---

Outside diff comments:
In
`@code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs`:
- Around line 288-318: The code fetches all score-entry rows into raw via
ToListAsync() before deduplicating, and Distinct() is case-sensitive; change the
LINQ to perform the distinct projection in SQL before materialization: move the
.Select(...) projection (projecting ArrInstanceId/Name/ItemType and a
case-normalized QualityProfileName, e.g. e.QualityProfileName.ToLower() or
ToUpper()) immediately before calling ToListAsync() so the database returns one
row per distinct profile name per instance, then perform the in-memory grouping
(the instances variable) using StringComparer.OrdinalIgnoreCase to collapse any
remaining case variants; look for _dataContext.CustomFormatScoreEntries, the
anonymous projection that creates Id/Name/ItemType/QualityProfileName, the
ToListAsync() call on raw, and the later Distinct()/OrderBy usage to apply this
change.

In
`@code/backend/Cleanuparr.Api/Features/Seeker/Controllers/SearchStatsController.cs`:
- Around line 120-135: Update the XML docs on the public GetEvents action to
fully document the expanded query contract: expand the one-line <summary> into a
brief description of what the endpoint returns and add <param> entries for page,
pageSize, instanceId, cycleId, search, sortBy, sortDirection, searchStatus,
searchType, searchReason and grabbed describing each parameter's purpose and
accepted values where applicable; explicitly document default values (page=1,
pageSize=50) and the default ordering behavior (sortBy defaults to
SearchEventsSortBy.Timestamp and sortDirection defaults to Desc) so consumers
know how results are filtered and ordered.

In
`@code/frontend/src/app/features/seeker-stats/upgrades-tab/upgrades-tab.component.ts`:
- Around line 199-223: The loadUpgrades method can let out-of-order responses
overwrite newer state; add a lightweight request guard by stamping each call
with a monotonic requestId (e.g., increment a private this._latestRequestId
before calling this.api.getRecentUpgrades) and capture that id in the subscribe
callbacks for next and error; inside next/error only apply this.upgrades.set,
this.totalRecords.set, this.loading.set, and this.toast.error if the captured id
equals this._latestRequestId, otherwise ignore the response. Update the
loadUpgrades method and declare the private this._latestRequestId field used by
the guard.

---

Nitpick comments:
In
`@code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs`:
- Around line 288-320: Add XML documentation comments to the public action that
returns instances (the GetInstances endpoint in CustomFormatScoreController)
describing purpose, parameters (if any), and the response shape; include a
<summary> that explains the richer payload, a <returns> describing that it
returns an OkObjectResult with an Instances collection containing Id, Name,
ItemType and QualityProfiles, and any <remarks> or <response> tags required by
project conventions so the public API aligns with other documented endpoints in
this controller.

In
`@code/backend/Cleanuparr.Persistence/Migrations/Data/20260420134250_AddLastUpgradedAtToCfScoreEntry.cs`:
- Around line 20-23: The migration currently calls migrationBuilder.CreateIndex
for "ix_custom_format_score_entries_last_upgraded_at" on table
"custom_format_score_entries" before performing the bulk backfill UPDATE (see
the backfill block around lines 27-58); move the CreateIndex call so it runs
after the backfill completes (or instead drop/recreate the index after the
UPDATE), ensuring the index creation occurs only once after rows are populated
to avoid per-row index updates during the backfill.

In `@code/frontend/src/app/core/api/search-stats.api.ts`:
- Around line 19-30: SearchEventsQuery currently types searchStatus, searchType,
and searchReason as plain string arrays; update them to enum/string-literal
types to match backend contracts and provide compile-time safety. Replace
searchStatus?: string[] with the appropriate enum/union type (e.g.,
SearchStatus[]), searchType?: string[] with SearchType[] and searchReason?:
string[] with SearchReason[] (or the existing string-literal unions), import or
reference the existing enums/types used elsewhere in the codebase, and update
any callers/tests to use the enum values instead of raw strings.

In `@code/frontend/src/styles/_page-animations.scss`:
- Around line 9-19: The shared mixin page-section-stagger should respect users'
reduced-motion preference: inside the mixin (mixin page-section-stagger) add a
prefers-reduced-motion: reduce branch that disables animations for the children
(e.g., set animation: none and remove animation-delay/transition behavior or set
transition-duration/animation-duration to 0) so pages using this mixin don't
need per-page overrides; ensure both the > * rule (animation: slide-up ...) and
the per-child animation-delay rules are overridden inside the `@media`
(prefers-reduced-motion: reduce) block to fully disable the staggered animation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 82e1f011-b780-45cb-95f3-4032b2c48116

📥 Commits

Reviewing files that changed from the base of the PR and between 7ed0f30 and 1c3b66c.

📒 Files selected for processing (47)
  • code/backend/Cleanuparr.Api.Tests/Features/Seeker/CustomFormatScoreControllerTests.cs
  • code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Responses/CustomFormatScoreEntryResponse.cs
  • code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs
  • code/backend/Cleanuparr.Api/Features/Seeker/Controllers/SearchStatsController.cs
  • code/backend/Cleanuparr.Domain/Enums/CfScoresSortBy.cs
  • code/backend/Cleanuparr.Domain/Enums/CfUpgradesSortBy.cs
  • code/backend/Cleanuparr.Domain/Enums/CutoffFilter.cs
  • code/backend/Cleanuparr.Domain/Enums/MonitoredFilter.cs
  • code/backend/Cleanuparr.Domain/Enums/SearchEventsSortBy.cs
  • code/backend/Cleanuparr.Domain/Enums/SortDirection.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Jobs/CustomFormatScoreSyncer.cs
  • code/backend/Cleanuparr.Persistence/DataContext.cs
  • code/backend/Cleanuparr.Persistence/Migrations/Data/20260420134250_AddLastUpgradedAtToCfScoreEntry.Designer.cs
  • code/backend/Cleanuparr.Persistence/Migrations/Data/20260420134250_AddLastUpgradedAtToCfScoreEntry.cs
  • code/backend/Cleanuparr.Persistence/Migrations/Data/DataContextModelSnapshot.cs
  • code/backend/Cleanuparr.Persistence/Models/State/CustomFormatScoreEntry.cs
  • code/frontend/src/app/core/api/cf-score.api.ts
  • code/frontend/src/app/core/api/search-stats.api.ts
  • code/frontend/src/app/core/directives/sticky-aware.directive.ts
  • code/frontend/src/app/features/dashboard/dashboard.component.scss
  • code/frontend/src/app/features/dashboard/dashboard.component.ts
  • code/frontend/src/app/features/events/events.component.html
  • code/frontend/src/app/features/events/events.component.scss
  • code/frontend/src/app/features/events/events.component.ts
  • code/frontend/src/app/features/logs-component/logs.component.html
  • code/frontend/src/app/features/logs-component/logs.component.scss
  • code/frontend/src/app/features/logs-component/logs.component.ts
  • code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.html
  • code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.scss
  • code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.ts
  • code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.html
  • code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.scss
  • code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.ts
  • code/frontend/src/app/features/seeker-stats/seeker-stats.component.scss
  • code/frontend/src/app/features/seeker-stats/upgrades-tab/upgrades-tab.component.html
  • code/frontend/src/app/features/seeker-stats/upgrades-tab/upgrades-tab.component.scss
  • code/frontend/src/app/features/seeker-stats/upgrades-tab/upgrades-tab.component.ts
  • code/frontend/src/app/features/strikes/strikes.component.html
  • code/frontend/src/app/features/strikes/strikes.component.scss
  • code/frontend/src/app/features/strikes/strikes.component.ts
  • code/frontend/src/app/ui/drawer/drawer.component.html
  • code/frontend/src/app/ui/drawer/drawer.component.scss
  • code/frontend/src/app/ui/drawer/drawer.component.ts
  • code/frontend/src/app/ui/index.ts
  • code/frontend/src/styles/_animations.scss
  • code/frontend/src/styles/_data-toolbar.scss
  • code/frontend/src/styles/_page-animations.scss

Comment thread code/backend/Cleanuparr.Domain/Enums/CfScoresSortBy.cs
Comment thread code/backend/Cleanuparr.Domain/Enums/CfUpgradesSortBy.cs
Comment thread code/backend/Cleanuparr.Domain/Enums/CutoffFilter.cs
Comment thread code/frontend/src/app/ui/drawer/drawer.component.html
Comment thread code/frontend/src/app/ui/drawer/drawer.component.scss
Comment thread code/frontend/src/app/ui/drawer/drawer.component.ts
@Flaminel Flaminel force-pushed the add_seeker_filters branch from 4b3a152 to 298881b Compare April 21, 2026 13:30
@Flaminel

Copy link
Copy Markdown
Contributor Author

@greptileai review

@Flaminel

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 21, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Cleanuparr Cleanuparr deleted a comment from coderabbitai Bot Apr 21, 2026
@coderabbitai

coderabbitai Bot commented Apr 21, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":404,"request":{"method":"PATCH","url":"https://api.github.com/repos/Cleanuparr/Cleanuparr/issues/comments/4287017501","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- walkthrough_start -->\n\n<details>\n<summary>📝 Walkthrough</summary>\n\n## Walkthrough\n\nThis PR adds typed filtering and sorting for Seeker endpoints, a LastUpgradedAt timestamp (DB + migration + model + backfill), refactors controller query logic (including SQL for upgrade detection), updates background sync to set LastUpgradedAt on score increases, and implements frontend drawer/sticky UI plus API/query type changes.\n\n## Changes\n\n|Cohort / File(s)|Summary|\n|---|---|\n|**Enums (backend/frontend)** <br> `code/backend/Cleanuparr.Domain/Enums/CfScoresSortBy.cs`, `code/backend/Cleanuparr.Domain/Enums/CfUpgradesSortBy.cs`, `.../CutoffFilter.cs`, `.../MonitoredFilter.cs`, `.../SearchEventsSortBy.cs`, `.../SortDirection.cs`, `code/frontend/src/app/core/api/*.ts`|Added multiple JSON-stringified enums for sort/filter domains; frontend re-exports/types updated to use these enums.|\n|**CustomFormatScore API & Responses** <br> `code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs`, `.../Contracts/Responses/CustomFormatScoreEntryResponse.cs`|Replaced string/bool query parameters with enum-driven filters and SortDirection; added `LastUpgradedAt` to response; introduced SQL-based upgrade detection helpers and ordering logic.|\n|**SearchStats API** <br> `code/backend/Cleanuparr.Api/Features/Seeker/Controllers/SearchStatsController.cs`|Extended `GetEvents` with enum-based sort, sortDirection, multi-valued status, searchType/searchReason, and tri-state grabbed filtering; ordering/pagination adjusted accordingly.|\n|**Database model & migration** <br> `code/backend/Cleanuparr.Persistence/Models/State/CustomFormatScoreEntry.cs`, `code/backend/Cleanuparr.Persistence/DataContext.cs`, `.../Migrations/Data/20260420134250_AddLastUpgradedAtToCfScoreEntry.*`, `DataContextModelSnapshot.cs`|Added nullable `LastUpgradedAt` property, UTC conversion and DB index; migration adds column, backfills LastUpgradedAt via score-history LAG-based SQL, and creates index.|\n|**Background job** <br> `code/backend/Cleanuparr.Infrastructure/Features/Jobs/CustomFormatScoreSyncer.cs`|When updating existing score, sets `LastUpgradedAt` to now if new score > previous CurrentScore.|\n|**Backend tests** <br> `code/backend/Cleanuparr.Api.Tests/Features/Seeker/CustomFormatScoreControllerTests.cs`, `.../SearchStatsControllerTests.cs`, `.../TestHelpers/*.cs`|Updated tests to use enum parameters and added many new cases for filter/sort behaviors; test helpers extended (e.g., qualityProfile, timestamp).|\n|**Frontend API layer** <br> `code/frontend/src/app/core/api/cf-score.api.ts`, `code/frontend/src/app/core/api/search-stats.api.ts`|Refactored APIs to accept query objects (`CfScoresQuery`, `CfScoreUpgradesQuery`, `SearchEventsQuery`), emit enum params conditionally, added `lastUpgradedAt` and `qualityProfiles` model fields.|\n|**Frontend components & UI** <br> `code/frontend/src/app/features/seeker-stats/*`, `.../events.component.*`, `.../logs.component.*`, `.../strikes.component.*`, `dashboard.component.ts`|Reworked Quality/Searches/Upgrades tabs to use applied/draft filter drawer, typed sort/direction, request deduplication tokens, and updated templates to use filter drawers and sticky-aware toolbars; minor dashboard call change.|\n|**Drawer + Sticky directive** <br> `code/frontend/src/app/ui/drawer/drawer.component.*`, `code/frontend/src/app/core/directives/sticky-aware.directive.ts`, `code/frontend/src/app/ui/index.ts`|Added DrawerComponent (focus management, backdrop/escape handling) and StickyAwareDirective (IntersectionObserver); re-exported Drawer.|\n|**Animations & SCSS** <br> `code/frontend/src/styles/_page-animations.scss`, `_animations.scss`, `_data-toolbar.scss`, multiple component scss files|Introduced `page-section-stagger` mixin, removed blur from `slide-up`, adjusted sticky header styles to apply effects only when `.is-stuck`, and migrated per-component stagger rules to the new mixin.|\n\n## Sequence Diagram(s)\n\n```mermaid\nsequenceDiagram\n  participant Client as Client (Browser)\n  participant Frontend as Frontend (Angular)\n  participant API as API (CustomFormatScoreController)\n  participant DB as Database\n  Client->>Frontend: Request recent upgrades (query object)\n  Frontend->>API: GET /api/seeker/custom-format-score/upgrades?params...\n  API->>DB: Execute CTE/LAG SQL to detect upgrade rows + COUNT for total\n  DB-->>API: Rows + total count\n  API-->>Frontend: Paginated upgrades response\n  Frontend-->>Client: Render upgrades, update UI\n```\n## Estimated code review effort\n\n🎯 4 (Complex) | ⏱️ ~45 minutes\n\n## Possibly related issues\n\n- Cleanuparr/Cleanuparr#568: Implements expanded filter/sort functionality for Seeker stats (backend controllers, enums, and frontend filter/drawer integration), matching the issue objective.\n\n## Poem\n\n> 🐰 Filters hop in tidy rows,  \n> > Enums guard the paths they chose,  \n> > Drawers slide and headers cling,  \n> > Scores rise, and timestamps sing,  \n> > A tidy burrow for queries grows. 🥕\n\n</details>\n\n<!-- walkthrough_end -->\n\n\n<!-- pre_merge_checks_walkthrough_start -->\n\n<details>\n<summary>🚥 Pre-merge checks | ✅ 4 | ❌ 1</summary>\n\n### ❌ Failed checks (1 warning)\n\n|     Check name     | Status     | Explanation                                                                           | Resolution                                                                         |\n| :----------------: | :--------- | :------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------- |\n| Docstring Coverage | ⚠️ Warning | Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |\n\n<details>\n<summary>✅ Passed checks (4 passed)</summary>\n\n|         Check name         | Status   | Explanation                                                                                                                                                         |\n| :------------------------: | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n|         Title check        | ✅ Passed | The PR title clearly and concisely summarizes the main change: adding sorting and filtering functionality to Seeker stats endpoints and UI.                         |\n|      Description check     | ✅ Passed | The PR description is minimal but relates to the changeset by referencing issue `#568`, which the work addresses through enhanced filtering and sorting capabilities. |\n|     Linked Issues check    | ✅ Passed | Check skipped because no linked issues were found for this pull request.                                                                                            |\n| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request.                                                                                            |\n\n</details>\n\n<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing Touches</summary>\n\n<details>\n<summary>📝 Generate docstrings</summary>\n\n- [ ] <!-- {\"checkboxId\": \"7962f53c-55bc-4827-bfbf-6a18da830691\"} --> Create stacked PR\n- [ ] <!-- {\"checkboxId\": \"3e1879ae-f29b-4d0d-8e06-d12b7ba33d98\"} --> Commit on current branch\n\n</details>\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-4288968435\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\", \"radioGroupId\": \"utg-output-choice-group-4288968435\"} -->   Commit unit tests in branch `add_seeker_filters`\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n\n<!-- tips_start -->\n\n---\n\n\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n\n<!-- internal state start -->\n\n\n<!-- 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 -->\n\n<!-- internal state end -->"},"request":{"retryCount":1,"signal":{}}},"response":{"url":"https://api.github.com/repos/Cleanuparr/Cleanuparr/issues/comments/4287017501","status":404,"headers":{"access-control-allow-origin":"*","access-control-expose-headers":"ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset","content-encoding":"gzip","content-security-policy":"default-src 'none'","content-type":"application/json; charset=utf-8","date":"Tue, 21 Apr 2026 13:43:29 GMT","referrer-policy":"origin-when-cross-origin, strict-origin-when-cross-origin","server":"github.com","strict-transport-security":"max-age=31536000; includeSubdomains; preload","transfer-encoding":"chunked","vary":"Accept-Encoding, Accept, X-Requested-With","x-accepted-github-permissions":"issues=write; pull_requests=write","x-content-type-options":"nosniff","x-frame-options":"deny","x-github-api-version-selected":"2022-11-28","x-github-media-type":"github.v3; format=json","x-github-request-id":"04B2:230FF3:1B62F3C:6E67FAD:69E77F01","x-ratelimit-limit":"5000","x-ratelimit-remaining":"4772","x-ratelimit-reset":"1776781237","x-ratelimit-resource":"core","x-ratelimit-used":"228","x-xss-protection":"0"},"data":{"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#update-an-issue-comment","status":"404"}}}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (6)
code/backend/Cleanuparr.Api.Tests/Features/Seeker/SearchStatsControllerTests.cs (3)

179-181: Use a fixed baseline timestamp for deterministic ordering.

Using DateTime.UtcNow three times can make this test clock-dependent. Prefer a single fixed UTC baseline and derive offsets from it.

Suggested tweak
-        AddSearchEvent(itemTitle: "Newest", timestamp: DateTime.UtcNow);
-        AddSearchEvent(itemTitle: "Oldest", timestamp: DateTime.UtcNow.AddHours(-2));
-        AddSearchEvent(itemTitle: "Middle", timestamp: DateTime.UtcNow.AddHours(-1));
+        var baseTime = new DateTime(2026, 1, 1, 12, 0, 0, DateTimeKind.Utc);
+        AddSearchEvent(itemTitle: "Newest", timestamp: baseTime);
+        AddSearchEvent(itemTitle: "Oldest", timestamp: baseTime.AddHours(-2));
+        AddSearchEvent(itemTitle: "Middle", timestamp: baseTime.AddHours(-1));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@code/backend/Cleanuparr.Api.Tests/Features/Seeker/SearchStatsControllerTests.cs`
around lines 179 - 181, The three AddSearchEvent calls use DateTime.UtcNow
separately causing non-deterministic ordering; fix by creating a single fixed
UTC baseline (e.g., DateTime baseline = new DateTime(2020,1,1,0,0,0,
DateTimeKind.Utc) or capture DateTime baseline = DateTime.UtcNow once) in
SearchStatsControllerTests and replace the timestamps with baseline,
baseline.AddHours(-2), and baseline.AddHours(-1) when calling
AddSearchEvent(itemTitle: "...", timestamp: ...).

192-203: Strengthen status-filter assertions beyond count-only checks.

TotalCount == 2 can still pass with wrong records. Assert the returned statuses or titles explicitly to verify filter correctness.

Suggested tweak
         body.GetProperty("TotalCount").GetInt32().ShouldBe(2);
+        var items = body.GetProperty("Items");
+        items.GetArrayLength().ShouldBe(2);
+        items.EnumerateArray().Any(i => i.GetProperty("ItemTitle").GetString() == "A").ShouldBeTrue();
+        items.EnumerateArray().Any(i => i.GetProperty("ItemTitle").GetString() == "B").ShouldBeTrue();

Based on learnings: "Write unit tests for new features and bug fixes with descriptive test names".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@code/backend/Cleanuparr.Api.Tests/Features/Seeker/SearchStatsControllerTests.cs`
around lines 192 - 203, The test
GetEvents_WithSearchStatusFilter_ReturnsOnlyMatchingStatuses currently only
asserts TotalCount == 2 which can miss wrong records; update the test in
SearchStatsControllerTests to also inspect the response array from
_controller.GetEvents (use GetResponseBody(result) and enumerate the events
collection) and assert that each returned item's searchStatus (or itemTitle)
matches one of the allowed enums SearchCommandStatus.Completed or
SearchCommandStatus.Failed (and/or assert the returned titles are exactly "A"
and "B"), so the filter logic in GetEvents is verified by content not just
count.

245-255: Add coverage for grabbed=false when SearchEventData is null.

The controller also includes events with no SearchEventData for grabbed=false (see SearchStatsController.cs line range 209-220), but this test currently covers only the empty grabbed-list branch.

Based on learnings: "Write unit tests for new features and bug fixes with descriptive test names".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@code/backend/Cleanuparr.Api.Tests/Features/Seeker/SearchStatsControllerTests.cs`
around lines 245 - 255, The test covers only events whose SearchEventData has an
empty grabbed list; add coverage for the branch where SearchEventData is null to
ensure grabbed=false includes events with no SearchEventData. Update or create a
test (e.g., alongside
GetEvents_WithGrabbedFalse_KeepsOnlyEventsWithoutGrabbedItems) that calls
AddSearchEvent with SearchEventData set to null (or a helper overload) and then
calls _controller.GetEvents(grabbed: false); assert the TotalCount and that the
returned Items includes the event whose SearchEventData was null using the same
body assertions (GetProperty("TotalCount") and
Items[...].GetProperty("ItemTitle")) to verify the controller handles null
SearchEventData correctly.
code/frontend/src/app/features/seeker-stats/upgrades-tab/upgrades-tab.component.scss (1)

81-87: Replace deprecated word-break: break-word with modern alternative.

The CSS value word-break: break-word is deprecated. The newer .cell-title__text class (lines 168-173) correctly uses overflow-wrap: anywhere, but this rule still uses the deprecated value.

♻️ Proposed fix
   &__title {
     font-weight: 500;
     color: var(--text-primary);
     padding: 0 var(--space-4) var(--space-2);
-    word-break: break-word;
+    overflow-wrap: anywhere;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@code/frontend/src/app/features/seeker-stats/upgrades-tab/upgrades-tab.component.scss`
around lines 81 - 87, The CSS rule for &__title uses the deprecated word-break:
break-word; replace it with the modern equivalent by removing word-break and
adding overflow-wrap: anywhere (or overflow-wrap: anywhere plus word-break:
normal if you want explicit behavior) to the &__title selector so it matches the
behavior used in .cell-title__text; update the &__title block in
upgrades-tab.component.scss accordingly to use overflow-wrap: anywhere and
ensure no deprecated properties remain.
code/frontend/src/app/core/directives/sticky-aware.directive.ts (1)

36-46: Consider handling the case where no scrollable parent exists.

When findScrollParent returns null, the observer uses the viewport as root. This works for most cases, but if the sticky element is deeply nested without any scroll container, the behavior might be unexpected.

This is a minor edge case for now since all current usages appear to be within scrollable shell containers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/frontend/src/app/core/directives/sticky-aware.directive.ts` around lines
36 - 46, findScrollParent currently returns null when no scrollable ancestor is
found which causes the IntersectionObserver to use the viewport unexpectedly;
change findScrollParent to return a fallback scroll container (e.g.
document.scrollingElement or document.documentElement) instead of null and
ensure the code that constructs the IntersectionObserver uses that returned
element as the root (reference findScrollParent and the code that creates the
IntersectionObserver) so deeply nested sticky elements use the page's scroll
container consistently.
code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs (1)

251-256: Consider reusing parameters to avoid redundant allocations.

BuildCommonParameters() is called twice (lines 269 and 280), creating two separate arrays with identical values. Since SqliteParameter objects can be reused across queries when they have the same values, consider extracting this to a local variable.

♻️ Suggested refactor
-        SqliteParameter[] BuildCommonParameters() => new[]
-        {
-            new SqliteParameter("@instanceId", instanceId.HasValue ? instanceId.Value : DBNull.Value),
-            new SqliteParameter("@search", (object?)searchPattern ?? DBNull.Value),
-            new SqliteParameter("@cutoff", (object?)cutoff ?? DBNull.Value),
-        };
+        var commonParams = new SqliteParameter[]
+        {
+            new("@instanceId", instanceId.HasValue ? instanceId.Value : DBNull.Value),
+            new("@search", (object?)searchPattern ?? DBNull.Value),
+            new("@cutoff", (object?)cutoff ?? DBNull.Value),
+        };

Then use commonParams directly in listParams and create a copy for countSql:

         SqliteParameter[] listParams =
         [
-            ..BuildCommonParameters(),
+            ..commonParams,
             new("@take", pageSize),
             new("@skip", (page - 1) * pageSize),
         ];

         // ...

-        int totalCount = await _dataContext.Database
-            .SqlQueryRaw<int>(countSql, BuildCommonParameters())
+        var countParams = commonParams.Select(p => new SqliteParameter(p.ParameterName, p.Value)).ToArray();
+        int totalCount = await _dataContext.Database
+            .SqlQueryRaw<int>(countSql, countParams)
             .FirstAsync();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs`
around lines 251 - 256, BuildCommonParameters() is being invoked twice creating
identical SqliteParameter arrays; call BuildCommonParameters() once into a local
variable (e.g., commonParams) and reuse that array when constructing listParams
(the query parameters used for the list SQL), and when building the count SQL
(countSql) create a shallow copy of commonParams before appending or modifying
any additional parameters so you don't mutate the reused array; reference the
SqliteParameter[] BuildCommonParameters(), listParams, and countSql symbols when
making this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@code/backend/Cleanuparr.Api.Tests/Features/Seeker/SearchStatsControllerTests.cs`:
- Around line 179-181: The three AddSearchEvent calls use DateTime.UtcNow
separately causing non-deterministic ordering; fix by creating a single fixed
UTC baseline (e.g., DateTime baseline = new DateTime(2020,1,1,0,0,0,
DateTimeKind.Utc) or capture DateTime baseline = DateTime.UtcNow once) in
SearchStatsControllerTests and replace the timestamps with baseline,
baseline.AddHours(-2), and baseline.AddHours(-1) when calling
AddSearchEvent(itemTitle: "...", timestamp: ...).
- Around line 192-203: The test
GetEvents_WithSearchStatusFilter_ReturnsOnlyMatchingStatuses currently only
asserts TotalCount == 2 which can miss wrong records; update the test in
SearchStatsControllerTests to also inspect the response array from
_controller.GetEvents (use GetResponseBody(result) and enumerate the events
collection) and assert that each returned item's searchStatus (or itemTitle)
matches one of the allowed enums SearchCommandStatus.Completed or
SearchCommandStatus.Failed (and/or assert the returned titles are exactly "A"
and "B"), so the filter logic in GetEvents is verified by content not just
count.
- Around line 245-255: The test covers only events whose SearchEventData has an
empty grabbed list; add coverage for the branch where SearchEventData is null to
ensure grabbed=false includes events with no SearchEventData. Update or create a
test (e.g., alongside
GetEvents_WithGrabbedFalse_KeepsOnlyEventsWithoutGrabbedItems) that calls
AddSearchEvent with SearchEventData set to null (or a helper overload) and then
calls _controller.GetEvents(grabbed: false); assert the TotalCount and that the
returned Items includes the event whose SearchEventData was null using the same
body assertions (GetProperty("TotalCount") and
Items[...].GetProperty("ItemTitle")) to verify the controller handles null
SearchEventData correctly.

In
`@code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs`:
- Around line 251-256: BuildCommonParameters() is being invoked twice creating
identical SqliteParameter arrays; call BuildCommonParameters() once into a local
variable (e.g., commonParams) and reuse that array when constructing listParams
(the query parameters used for the list SQL), and when building the count SQL
(countSql) create a shallow copy of commonParams before appending or modifying
any additional parameters so you don't mutate the reused array; reference the
SqliteParameter[] BuildCommonParameters(), listParams, and countSql symbols when
making this change.

In `@code/frontend/src/app/core/directives/sticky-aware.directive.ts`:
- Around line 36-46: findScrollParent currently returns null when no scrollable
ancestor is found which causes the IntersectionObserver to use the viewport
unexpectedly; change findScrollParent to return a fallback scroll container
(e.g. document.scrollingElement or document.documentElement) instead of null and
ensure the code that constructs the IntersectionObserver uses that returned
element as the root (reference findScrollParent and the code that creates the
IntersectionObserver) so deeply nested sticky elements use the page's scroll
container consistently.

In
`@code/frontend/src/app/features/seeker-stats/upgrades-tab/upgrades-tab.component.scss`:
- Around line 81-87: The CSS rule for &__title uses the deprecated word-break:
break-word; replace it with the modern equivalent by removing word-break and
adding overflow-wrap: anywhere (or overflow-wrap: anywhere plus word-break:
normal if you want explicit behavior) to the &__title selector so it matches the
behavior used in .cell-title__text; update the &__title block in
upgrades-tab.component.scss accordingly to use overflow-wrap: anywhere and
ensure no deprecated properties remain.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a1a032c9-aaae-4d70-b5ce-b1b7f916ab7d

📥 Commits

Reviewing files that changed from the base of the PR and between 1c3b66c and 298881b.

📒 Files selected for processing (50)
  • code/backend/Cleanuparr.Api.Tests/Features/Seeker/CustomFormatScoreControllerTests.cs
  • code/backend/Cleanuparr.Api.Tests/Features/Seeker/SearchStatsControllerTests.cs
  • code/backend/Cleanuparr.Api.Tests/Features/Seeker/TestHelpers/SeekerTestDataFactory.cs
  • code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Responses/CustomFormatScoreEntryResponse.cs
  • code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs
  • code/backend/Cleanuparr.Api/Features/Seeker/Controllers/SearchStatsController.cs
  • code/backend/Cleanuparr.Domain/Enums/CfScoresSortBy.cs
  • code/backend/Cleanuparr.Domain/Enums/CfUpgradesSortBy.cs
  • code/backend/Cleanuparr.Domain/Enums/CutoffFilter.cs
  • code/backend/Cleanuparr.Domain/Enums/MonitoredFilter.cs
  • code/backend/Cleanuparr.Domain/Enums/SearchEventsSortBy.cs
  • code/backend/Cleanuparr.Domain/Enums/SortDirection.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Jobs/CustomFormatScoreSyncer.cs
  • code/backend/Cleanuparr.Persistence/DataContext.cs
  • code/backend/Cleanuparr.Persistence/Migrations/Data/20260420134250_AddLastUpgradedAtToCfScoreEntry.Designer.cs
  • code/backend/Cleanuparr.Persistence/Migrations/Data/20260420134250_AddLastUpgradedAtToCfScoreEntry.cs
  • code/backend/Cleanuparr.Persistence/Migrations/Data/DataContextModelSnapshot.cs
  • code/backend/Cleanuparr.Persistence/Models/State/CustomFormatScoreEntry.cs
  • code/frontend/src/app/core/api/cf-score.api.ts
  • code/frontend/src/app/core/api/search-stats.api.ts
  • code/frontend/src/app/core/directives/sticky-aware.directive.ts
  • code/frontend/src/app/core/models/search-stats.models.ts
  • code/frontend/src/app/features/dashboard/dashboard.component.scss
  • code/frontend/src/app/features/dashboard/dashboard.component.ts
  • code/frontend/src/app/features/events/events.component.html
  • code/frontend/src/app/features/events/events.component.scss
  • code/frontend/src/app/features/events/events.component.ts
  • code/frontend/src/app/features/logs-component/logs.component.html
  • code/frontend/src/app/features/logs-component/logs.component.scss
  • code/frontend/src/app/features/logs-component/logs.component.ts
  • code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.html
  • code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.scss
  • code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.ts
  • code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.html
  • code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.scss
  • code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.ts
  • code/frontend/src/app/features/seeker-stats/seeker-stats.component.scss
  • code/frontend/src/app/features/seeker-stats/upgrades-tab/upgrades-tab.component.html
  • code/frontend/src/app/features/seeker-stats/upgrades-tab/upgrades-tab.component.scss
  • code/frontend/src/app/features/seeker-stats/upgrades-tab/upgrades-tab.component.ts
  • code/frontend/src/app/features/strikes/strikes.component.html
  • code/frontend/src/app/features/strikes/strikes.component.scss
  • code/frontend/src/app/features/strikes/strikes.component.ts
  • code/frontend/src/app/ui/drawer/drawer.component.html
  • code/frontend/src/app/ui/drawer/drawer.component.scss
  • code/frontend/src/app/ui/drawer/drawer.component.ts
  • code/frontend/src/app/ui/index.ts
  • code/frontend/src/styles/_animations.scss
  • code/frontend/src/styles/_data-toolbar.scss
  • code/frontend/src/styles/_page-animations.scss
✅ Files skipped from review due to trivial changes (18)
  • code/frontend/src/app/features/logs-component/logs.component.html
  • code/frontend/src/app/features/strikes/strikes.component.ts
  • code/frontend/src/app/ui/index.ts
  • code/frontend/src/app/features/events/events.component.html
  • code/frontend/src/app/features/dashboard/dashboard.component.ts
  • code/frontend/src/app/features/logs-component/logs.component.ts
  • code/frontend/src/app/core/models/search-stats.models.ts
  • code/backend/Cleanuparr.Domain/Enums/CfUpgradesSortBy.cs
  • code/backend/Cleanuparr.Persistence/Migrations/Data/DataContextModelSnapshot.cs
  • code/backend/Cleanuparr.Domain/Enums/SortDirection.cs
  • code/backend/Cleanuparr.Persistence/Models/State/CustomFormatScoreEntry.cs
  • code/backend/Cleanuparr.Domain/Enums/CutoffFilter.cs
  • code/backend/Cleanuparr.Domain/Enums/CfScoresSortBy.cs
  • code/frontend/src/styles/_page-animations.scss
  • code/frontend/src/app/ui/drawer/drawer.component.scss
  • code/frontend/src/app/features/strikes/strikes.component.html
  • code/backend/Cleanuparr.Domain/Enums/MonitoredFilter.cs
  • code/backend/Cleanuparr.Domain/Enums/SearchEventsSortBy.cs
🚧 Files skipped from review as they are similar to previous changes (10)
  • code/backend/Cleanuparr.Persistence/DataContext.cs
  • code/backend/Cleanuparr.Infrastructure/Features/Jobs/CustomFormatScoreSyncer.cs
  • code/frontend/src/styles/_animations.scss
  • code/frontend/src/app/features/dashboard/dashboard.component.scss
  • code/frontend/src/app/features/strikes/strikes.component.scss
  • code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.scss
  • code/frontend/src/app/features/logs-component/logs.component.scss
  • code/backend/Cleanuparr.Api/Features/Seeker/Controllers/SearchStatsController.cs
  • code/frontend/src/app/core/api/search-stats.api.ts
  • code/frontend/src/app/features/events/events.component.ts

@Flaminel Flaminel merged commit 02a07d4 into main Apr 25, 2026
13 checks passed
@Flaminel Flaminel deleted the add_seeker_filters branch April 25, 2026 08:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants