feat(filters): tag/board actions + new match fields + priority + preview#78
Merged
Conversation
Extends the rules engine in a backwards-compatible way. Every existing
filter row still works (DEFAULT 100 priority, empty action_value).
Engine (internal/filters):
- New fields: tags, feed_id, published_at, has_image.
- New op: newer_than (duration, only valid with published_at).
- New actions: tag, add_to_board. Each carries its payload in the new
Filter.ActionValue column — tag name for `tag`, board id (decimal
string) for `add_to_board`.
- Field/op compatibility enforced in Match.Validate (numeric / bool /
duration fields don't accept string ops, and vice versa).
- ValidateActionWithValue checks that actions needing a payload have
one. Used at the API layer; the engine's Apply silently skips bad
payloads so a malformed rule never breaks ingest.
- Matches signature gains time.Time for the relative-date op (callers
in the hot path pass time.Now(); tests inject a frozen value).
- Apply now sorts rules by priority asc, id asc, deterministically. The
Outcome struct gains Tags []string and BoardIDs []int64; both are
deduped additively so multiple matching rules contribute distinct
tags / boards.
Storage:
- migration 0016_filters_extended: ALTER TABLE adds priority (default
100) and action_value (default ''), plus
idx_filters_user_prio(user_id, enabled, priority).
- store/filters.go: all CRUD paths carry both new columns. ListFilters
+ ListActiveFilters now ORDER BY priority ASC, id ASC.
- store/filter_preview.go: new PreviewFilter(userID, match, sinceDays)
walks the user's articles over the window and counts matches via the
same engine — never duplicates SQL logic.
Poller integration (internal/poller):
- applyFiltersForUser now passes time.Now() and follows up with
AddArticleTag / AddArticleToBoard calls for the new actions.
- AddArticleToBoard's existing per-user ownership check provides
cross-user safety; an unrelated board id silently no-ops (ErrNotFound).
API:
- filterReq carries priority (pointer) + action_value (string). Both
Create and Update validate action_value via the new
ValidateActionWithValue helper.
- POST /api/filters/preview returns {count} for a given match_json.
Frontend (web):
- types: Filter.action expanded to include tag / add_to_board; Filter
gains priority + action_value; FilterMatch.field/op expanded.
- api: createFilter / updateFilter accept the new fields; new
api.previewFilter(match_json, since_days).
- FilterManager.svelte: incremental — the existing 4-field, 4-op,
3-action form gains conditional inputs for the new fields (feed_id
dropdown, has_image true/false, published_at duration), the new
actions (tag → text input, add_to_board → board dropdown), a
priority number input, and a "Preview matches (last 7 days)" button
that calls the new endpoint.
Skipped for follow-up:
- AND/OR/NOT boolean composition (would require new UI / new wire
format envelope).
- notify action — pairs with #77 (web push) in a follow-up.
- Webhook action — separate concern.
2 tasks
brandonhon
added a commit
that referenced
this pull request
Jun 2, 2026
…iew (#78) Extends the rules engine backwards-compatibly. Every existing filter row still works. ## Engine - **New fields:** `tags`, `feed_id`, `published_at`, `has_image` - **New op:** `newer_than` (duration, only valid with `published_at`) - **New actions:** `tag`, `add_to_board`. Payload lives in `Filter.ActionValue` — tag name or board id (decimal string). - **Field/op compatibility** enforced in `Match.Validate` (numeric / bool / duration fields don't accept string ops). - **`ValidateActionWithValue`** checks payload at the API layer; the engine's `Apply` silently skips bad payloads so a malformed rule never breaks ingest. - **`Matches`** signature gains `time.Time` for the relative-date op (tests inject a frozen value). - **`Apply`** sorts rules by `priority ASC, id ASC` deterministically. `Outcome` gains `Tags []string` and `BoardIDs []int64` — additive, deduped. ## Storage - Migration `0016_filters_extended`: `priority` (DEFAULT 100), `action_value` (DEFAULT ''), `idx_filters_user_prio(user_id, enabled, priority)`. - All CRUD paths carry both new columns. `ListFilters` + `ListActiveFilters` ORDER BY priority ASC, id ASC. - New `store.PreviewFilter` walks the user's articles over a window and counts matches via the same engine — single source of truth. ## API - `filterReq` carries `priority` (pointer) + `action_value`. Both create + update validate via `ValidateActionWithValue`. - New: `POST /api/filters/preview` → `{count}` for a given `match_json`. ## Poller - `applyFiltersForUser` follows up with `AddArticleTag` / `AddArticleToBoard` for the new actions. `AddArticleToBoard`'s existing per-user ownership check provides cross-user safety. ## Frontend - Types: `Filter.action` expanded; `Filter` gains `priority` + `action_value`; `FilterMatch.field/op` expanded. - `FilterManager.svelte` incremental: conditional inputs (feed dropdown for `feed_id`, true/false for `has_image`, duration text for `published_at`), tag-name / board-dropdown for new actions, priority number input, **Preview matches (last 7 days)** button. ## Skipped (follow-ups) - AND/OR/NOT composition (needs new UI + wire-format envelope) - `notify` action — pairs with #77 (web push) in a separate follow-up - Webhook action ## Test plan - [x] `go test -race ./...` green (existing dedup + filter tests + new engine cases) - [x] `go vet ./...` clean - [x] `svelte-check` 0 errors - [x] `vite build` + vitest 21/21 - [ ] Manual: create a `tag` rule on "title contains crypto" → refresh a feed → see the tag appear - [ ] Manual: create an `add_to_board` rule → confirm article lands in the board - [ ] Manual: published_at newer_than 24h matches recent articles only - [ ] Manual: preview button shows realistic count Co-authored-by: Brandon Honeycutt <bh+claude@hny.io>
brandonhon
added a commit
that referenced
this pull request
Jun 4, 2026
…iew (#78) Extends the rules engine backwards-compatibly. Every existing filter row still works. - **New fields:** `tags`, `feed_id`, `published_at`, `has_image` - **New op:** `newer_than` (duration, only valid with `published_at`) - **New actions:** `tag`, `add_to_board`. Payload lives in `Filter.ActionValue` — tag name or board id (decimal string). - **Field/op compatibility** enforced in `Match.Validate` (numeric / bool / duration fields don't accept string ops). - **`ValidateActionWithValue`** checks payload at the API layer; the engine's `Apply` silently skips bad payloads so a malformed rule never breaks ingest. - **`Matches`** signature gains `time.Time` for the relative-date op (tests inject a frozen value). - **`Apply`** sorts rules by `priority ASC, id ASC` deterministically. `Outcome` gains `Tags []string` and `BoardIDs []int64` — additive, deduped. - Migration `0016_filters_extended`: `priority` (DEFAULT 100), `action_value` (DEFAULT ''), `idx_filters_user_prio(user_id, enabled, priority)`. - All CRUD paths carry both new columns. `ListFilters` + `ListActiveFilters` ORDER BY priority ASC, id ASC. - New `store.PreviewFilter` walks the user's articles over a window and counts matches via the same engine — single source of truth. - `filterReq` carries `priority` (pointer) + `action_value`. Both create + update validate via `ValidateActionWithValue`. - New: `POST /api/filters/preview` → `{count}` for a given `match_json`. - `applyFiltersForUser` follows up with `AddArticleTag` / `AddArticleToBoard` for the new actions. `AddArticleToBoard`'s existing per-user ownership check provides cross-user safety. - Types: `Filter.action` expanded; `Filter` gains `priority` + `action_value`; `FilterMatch.field/op` expanded. - `FilterManager.svelte` incremental: conditional inputs (feed dropdown for `feed_id`, true/false for `has_image`, duration text for `published_at`), tag-name / board-dropdown for new actions, priority number input, **Preview matches (last 7 days)** button. - AND/OR/NOT composition (needs new UI + wire-format envelope) - `notify` action — pairs with #77 (web push) in a separate follow-up - Webhook action - [x] `go test -race ./...` green (existing dedup + filter tests + new engine cases) - [x] `go vet ./...` clean - [x] `svelte-check` 0 errors - [x] `vite build` + vitest 21/21 - [ ] Manual: create a `tag` rule on "title contains crypto" → refresh a feed → see the tag appear - [ ] Manual: create an `add_to_board` rule → confirm article lands in the board - [ ] Manual: published_at newer_than 24h matches recent articles only - [ ] Manual: preview button shows realistic count Co-authored-by: Brandon Honeycutt <bh+claude@hny.io>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Extends the rules engine backwards-compatibly. Every existing filter row still works.
Engine
tags,feed_id,published_at,has_imagenewer_than(duration, only valid withpublished_at)tag,add_to_board. Payload lives inFilter.ActionValue— tag name or board id (decimal string).Match.Validate(numeric / bool / duration fields don't accept string ops).ValidateActionWithValuechecks payload at the API layer; the engine'sApplysilently skips bad payloads so a malformed rule never breaks ingest.Matchessignature gainstime.Timefor the relative-date op (tests inject a frozen value).Applysorts rules bypriority ASC, id ASCdeterministically.OutcomegainsTags []stringandBoardIDs []int64— additive, deduped.Storage
0016_filters_extended:priority(DEFAULT 100),action_value(DEFAULT ''),idx_filters_user_prio(user_id, enabled, priority).ListFilters+ListActiveFiltersORDER BY priority ASC, id ASC.store.PreviewFilterwalks the user's articles over a window and counts matches via the same engine — single source of truth.API
filterReqcarriespriority(pointer) +action_value. Both create + update validate viaValidateActionWithValue.POST /api/filters/preview→{count}for a givenmatch_json.Poller
applyFiltersForUserfollows up withAddArticleTag/AddArticleToBoardfor the new actions.AddArticleToBoard's existing per-user ownership check provides cross-user safety.Frontend
Filter.actionexpanded;Filtergainspriority+action_value;FilterMatch.field/opexpanded.FilterManager.svelteincremental: conditional inputs (feed dropdown forfeed_id, true/false forhas_image, duration text forpublished_at), tag-name / board-dropdown for new actions, priority number input, Preview matches (last 7 days) button.Skipped (follow-ups)
notifyaction — pairs with feat(push): Web Push (VAPID) infrastructure + test-send button #77 (web push) in a separate follow-upTest plan
go test -race ./...green (existing dedup + filter tests + new engine cases)go vet ./...cleansvelte-check0 errorsvite build+ vitest 21/21tagrule on "title contains crypto" → refresh a feed → see the tag appearadd_to_boardrule → confirm article lands in the board