feat(438,435,434): RuleBuilder validation, relative-date operators, DB-sourced facet typeahead #577

Merged
timothy merged 19 commits from feat/rulebuilder-bundle into main 2026-07-23 21:16:55 +02:00
Owner

Bundle of three RuleBuilder issues, built on one branch (shared RuleBuilder.tsx).

fixes #438
fixes #435
fixes #434

#438 — validation & polish (frontend)

  • Incomplete between / empty-text rules are invalid: blocked at Save (groupHasErrors) and never emit malformed Lucene (compile returns '', filtered) — including nested all-invalid subgroups (no more "()").
  • Single-child groups normalize matchall (normalizeGroup) so they round-trip; the any/all toggle is hidden for <2 children.
  • Non-blocking warning when the top group is entirely negative.

#435 — relative-date operators (frontend only)

  • inLast/notInLast + a unit picker, compile/parse-mapped onto the pre-existing released_inthelast/added_inthelast (+notinthelast) CustomMultiFieldQueryParser macros. No backend change. Strict /^\d+$/ operand validation (rejects 1e2/7.0 that would break the round-trip).

#434 — facet-value typeahead

  • New GET /api/v1/search/fields/{name}/values?q=&limit= returning distinct whole values from the database (per-field EF queries over shared lookup entities / metadata columns), allow-listed to facet fields — title/show_title/album_artist and non-text/unknown → 404; content_rating split on /; state/video_dynamic_range from fixed enums; case-insensitive prefix, limit clamped [1,50].
  • Frontend autocomplete combobox for text-field value inputs with a free-text fallback.
  • (Initially prototyped over the Lucene term dictionary; reworked to DB-distinct because analyzed TextFields only yield lowercased word fragments.)

Verification

  • Full web suite 980/980; C# search handler 9/9 (EF-backed); dotnet build clean; format/BOM clean.
  • Independent reviews per task + a Fable/opus whole-branch pass + a dedicated opus review of the DB-sourcing rework (all MERGEABLE); must-fixes applied.
  • Live-E2E (booted from source): supported fields 200; state/video_dynamic_range return real whole values; case-insensitive prefix works live (state?q=nNormal); title/show_title/album_artist/minutes/plot/unknown → 404.

Docs

docs/decisions.md (api.search-field-values, rulebuilder.relative-date-macros) + catalog, docs/spa-conventions.md §12, docs/api-conventions.md, regenerated OpenAPI (v1.json/v1.d.ts/endpoint-index.md).

🤖 Generated with Claude Code

Bundle of three RuleBuilder issues, built on one branch (shared `RuleBuilder.tsx`). fixes #438 fixes #435 fixes #434 ## #438 — validation & polish (frontend) - Incomplete `between` / empty-text rules are invalid: blocked at Save (`groupHasErrors`) and never emit malformed Lucene (`compile` returns `''`, filtered) — including nested all-invalid subgroups (no more `"()"`). - Single-child groups normalize `match`→`all` (`normalizeGroup`) so they round-trip; the any/all toggle is hidden for <2 children. - Non-blocking warning when the top group is entirely negative. ## #435 — relative-date operators (frontend only) - `inLast`/`notInLast` + a `unit` picker, compile/parse-mapped onto the **pre-existing** `released_inthelast`/`added_inthelast` (+`notinthelast`) `CustomMultiFieldQueryParser` macros. No backend change. Strict `/^\d+$/` operand validation (rejects `1e2`/`7.0` that would break the round-trip). ## #434 — facet-value typeahead - New `GET /api/v1/search/fields/{name}/values?q=&limit=` returning **distinct whole values from the database** (per-field EF queries over shared lookup entities / metadata columns), allow-listed to facet fields — `title`/`show_title`/`album_artist` and non-text/unknown → 404; `content_rating` split on `/`; `state`/`video_dynamic_range` from fixed enums; case-insensitive prefix, `limit` clamped [1,50]. - Frontend autocomplete combobox for text-field value inputs with a free-text fallback. - (Initially prototyped over the Lucene term dictionary; reworked to DB-distinct because analyzed `TextField`s only yield lowercased word fragments.) ## Verification - Full web suite **980/980**; C# search handler **9/9** (EF-backed); `dotnet build` clean; format/BOM clean. - Independent reviews per task + a Fable/opus whole-branch pass + a dedicated opus review of the DB-sourcing rework (all MERGEABLE); must-fixes applied. - **Live-E2E** (booted from source): supported fields 200; `state`/`video_dynamic_range` return real whole values; case-insensitive prefix works live (`state?q=n`→`Normal`); `title`/`show_title`/`album_artist`/`minutes`/`plot`/unknown → 404. ## Docs `docs/decisions.md` (`api.search-field-values`, `rulebuilder.relative-date-macros`) + catalog, `docs/spa-conventions.md` §12, `docs/api-conventions.md`, regenerated OpenAPI (`v1.json`/`v1.d.ts`/`endpoint-index.md`). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
timothy added 19 commits 2026-07-23 21:00:29 +02:00
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds GET /api/v1/search/fields/{name}/values?q=&limit= — the backend slice of the
visual rule builder's facet-value typeahead (#434). Enumerates distinct Lucene term
values for a text field via MultiFields.GetTerms + TermsEnum, filtered by a
case-insensitive prefix, limit clamped to [1,50]. 404s when the field is absent from
SearchFieldCatalog or is not type "text". ElasticSearchIndex (the optional external
backend) throws NotSupportedException for this method — its text fields are analyzed,
not keyword-mapped, so a terms aggregation isn't safe to guess at without verifying
against a live cluster.

Regenerated OpenAPI trio (v1.json, v1.d.ts, endpoint-index.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- validation.ts: ruleError now uses /^\d+$/ regex instead of Number.isInteger
- dateMacro.ts: isValidN now uses /^\d+$/ regex instead of Number.isInteger
- Prevents accepting scientific/decimal notation (e.g. '1e2', '7.0') that break compile↔parse contract
- Added tests for both invalid cases returning null/error

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Lucene term dictionary stores lowercased word tokens for analyzed text
fields ("Science Fiction" -> science/fiction), so the typeahead was
suggesting fragments instead of whole values. GetSearchFieldValuesHandler
now injects IDbContextFactory<TvContext> and resolves an explicit
per-field-name distinct-values query (genre/studio/director/writer/actor/
artist/tag/network/collection/video_codec/album), with state and
video_dynamic_range computed in memory and content_rating split on '/' to
match what search actually matches on. title/show_title/album_artist have
no distinct source and now correctly 404 (free-text fallback), same as
before. Reverts the GetFieldValues additions to ISearchIndex/
LuceneSearchIndex/ElasticSearchIndex back to their pre-#434 state (BOM
stripped per #311, otherwise byte-identical). Endpoint shape, DTO,
controller, and OpenAPI are unchanged (no diff from
./scripts/update-openapi.sh).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Edits the api.search-field-values decision record's rationale prose in
place (same key, same date, not a reversal) to describe the DB-sourced
per-field distinct-values design and the narrowed allow-list, replacing
the superseded Lucene-term-dictionary description. Updates the endpoint's
api-conventions.md entry the same way. Regenerated docs/decisions/README.md
via build_decisions_catalog.py; decisions_validate.py passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ISearchIndex.cs / LuceneSearchIndex.cs still carried a BOM after the prior
commit — the strip ran after `git add`, so the staged (BOM'd) content was
what got committed. No content change beyond the BOM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The GetFieldValues additions were reverted in the DB-sourcing rework; these two
files had only an incidental BOM strip left, which pulled unrelated pre-existing
whitespace debt into the scoped format gate. Restore byte-identical to origin/main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(434): accurate endpoint description (DB-sourced) + tests for case-insensitivity, tag exclusion, distinct
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 12s
PR Gates / Docs update reminder (pull_request) Successful in 16s
PR Gates / decisions lifecycle (pull_request) Successful in 19s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m24s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m43s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 13m40s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 15m38s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 15m50s
7e6861a928
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Owner

Review-verdict: MERGEABLE @ 7e6861a9

Gates: per-task reviews (all clean) + opus whole-branch pass + dedicated opus review of the DB-sourcing rework — all MERGEABLE, must-fixes applied (strict relative-date N validation; ES graceful-empty [later removed in DB rework]; accurate endpoint description). Local gate green (web 980/980, C# search 9/9, build/format/BOM clean). Live-E2E confirmed the DB-sourced endpoint + narrowed allow-list.

Review-verdict: MERGEABLE @ 7e6861a9 Gates: per-task reviews (all clean) + opus whole-branch pass + dedicated opus review of the DB-sourcing rework — all MERGEABLE, must-fixes applied (strict relative-date N validation; ES graceful-empty [later removed in DB rework]; accurate endpoint description). Local gate green (web 980/980, C# search 9/9, build/format/BOM clean). Live-E2E confirmed the DB-sourced endpoint + narrowed allow-list.
timothy merged commit aca70b8398 into main 2026-07-23 21:16:55 +02:00
timothy deleted branch feat/rulebuilder-bundle 2026-07-23 21:16:56 +02:00
Sign in to join this conversation.