Skip to content

feat(filters): tag/board actions + new match fields + priority + preview#78

Merged
brandonhon merged 1 commit into
developfrom
feat/filters-extended
Jun 1, 2026
Merged

feat(filters): tag/board actions + new match fields + priority + preview#78
brandonhon merged 1 commit into
developfrom
feat/filters-extended

Conversation

@brandonhon
Copy link
Copy Markdown
Owner

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)

Test plan

  • go test -race ./... green (existing dedup + filter tests + new engine cases)
  • go vet ./... clean
  • svelte-check 0 errors
  • 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

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.
@brandonhon brandonhon merged commit 1e65eaf into develop Jun 1, 2026
8 checks passed
@brandonhon brandonhon deleted the feat/filters-extended branch June 1, 2026 03:44
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>
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.

1 participant