Scope reset-trackers by keyword and report progress

The reset-trackers operator command reset every uncategorised,
non-excluded pattern of a banner and printed only a single summary
line once the transaction committed, giving no feedback during long
rebuilds.

Add a --keyword flag that scopes both the glob decomposition and the
mapping reset to patterns whose pattern or display name contains the
substring. The match lives in a new TrackerPatternFilter.WithPatternKeyword
field so it runs in SQL and is shared by the glob load and the
ResetAndRequestMappingByCookieCategoryID update, keeping the two in
lockstep. The banner-wide pattern-analysis re-arm is left unscoped.

Thread an optional progress callback through ResetBannerTrackers so the
command streams per-phase updates (category load, per-glob decomposition,
mapping reset, analysis re-arm) as the work runs.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-06-09 19:40:16 +02:00
parent 5415b2d438
commit faca86a022
5 changed files with 95 additions and 10 deletions

View File

@@ -1390,14 +1390,19 @@ WHERE
// iterating on the mapping agent. Excluded patterns are left untouched -
// exclusion is a deliberate suppression. The cookie_category_id key
// scopes the reset to the uncategorised category the caller resolves;
// the Scoper keeps it tenant-isolated. Returns the number of patterns
// reset.
// the Scoper keeps it tenant-isolated. When keyword is non-nil and
// non-empty, the reset is further restricted to patterns whose pattern or
// display name contains it (case-insensitive). Returns the number of
// patterns reset.
func (tps *TrackerPatterns) ResetAndRequestMappingByCookieCategoryID(
ctx context.Context,
tx pg.Tx,
scope Scoper,
cookieCategoryID gid.GID,
keyword *string,
) (int64, error) {
filter := NewTrackerPatternFilter(nil, nil, nil).WithPatternKeyword(keyword)
q := `
UPDATE tracker_patterns
SET
@@ -1410,12 +1415,14 @@ WHERE
%s
AND cookie_category_id = @cookie_category_id
AND excluded = false
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment())
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
args := pgx.StrictNamedArgs{"cookie_category_id": cookieCategoryID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, filter.SQLArguments())
result, err := tx.Exec(ctx, q, args)
if err != nil {

View File

@@ -24,6 +24,7 @@ type TrackerPatternFilter struct {
cookieCategoryID *gid.GID
excluded *bool
query *string
patternKeyword *string
source *CookieSource
trackerType *TrackerType
thirdPartyID *gid.GID
@@ -47,6 +48,16 @@ func (f *TrackerPatternFilter) WithQuery(query *string) *TrackerPatternFilter {
return f
}
// WithPatternKeyword restricts the result to patterns whose pattern or
// display name contains keyword (case-insensitive). It differs from
// WithQuery, which matches display name or description: this targets the
// raw pattern so operators can select by the matched domain or cookie
// name. A nil or empty keyword disables the filter.
func (f *TrackerPatternFilter) WithPatternKeyword(keyword *string) *TrackerPatternFilter {
f.patternKeyword = keyword
return f
}
func (f *TrackerPatternFilter) WithSource(source *CookieSource) *TrackerPatternFilter {
f.source = source
return f
@@ -102,6 +113,13 @@ func (f *TrackerPatternFilter) SQLFragment() string {
ELSE TRUE
END
AND
CASE
WHEN @filter_pattern_keyword::text IS NOT NULL AND @filter_pattern_keyword::text != '' THEN
(pattern ILIKE '%' || @filter_pattern_keyword || '%'
OR display_name ILIKE '%' || @filter_pattern_keyword || '%')
ELSE TRUE
END
AND
CASE
WHEN @has_source_filter::boolean = false THEN TRUE
WHEN @has_source_filter::boolean = true THEN
@@ -148,6 +166,7 @@ func (f *TrackerPatternFilter) SQLArguments() pgx.StrictNamedArgs {
"has_excluded_filter": false,
"filter_excluded": nil,
"filter_query": nil,
"filter_pattern_keyword": nil,
"has_source_filter": false,
"filter_source": nil,
"has_tracker_type_filter": false,
@@ -177,6 +196,10 @@ func (f *TrackerPatternFilter) SQLArguments() pgx.StrictNamedArgs {
args["filter_query"] = *f.query
}
if f.patternKeyword != nil {
args["filter_pattern_keyword"] = *f.patternKeyword
}
if f.source != nil {
args["has_source_filter"] = true
args["filter_source"] = string(*f.source)