Archaeology, prompted by the user asking when it broke: the builder's save NEVER worked. The page was born (2026-02-19,a92bf99a) posting auto_assign_rules as a JSON-encoded STRING into an endpoint that has bound []Rule since its creation (2026-01-31,6495cc2c), plus fields (manual_book_ids, match_type, library_id, show_on_dashboard) the endpoint never accepted. The preview button hit a real endpoint, so the page looked alive while Save always failed at request binding. Verified live before this change: the page's exact payload 400'd "invalid request" against the unmodified server. The fix adapts the builder's payload at write time — the legacy rules vocabulary converts to a filter tree (RulesToFilterTree restored from the retirement commit as the single write path, unit test restored with it): - match_type seeds the converted tree's root mode (any default). - manual_book_ids attach to the created collection after create (unparsable ids skipped — the AddBooks leniency). - show_on_dashboard persists (new create field). - Converted trees validate under ParseFilterTree: an unhonorable rule is a 400 at write naming the problem, never a stored 500 at render. Dashboard sections: smart user sections now evaluate their tree LIVE — GetUserCollectionItems merges stored membership with the composed engine's matches (deduped, library-scoped, capped at the section limit). This exposed a REAL production wiring bug: every production NewDashboardService call omitted the search service, so tree evaluation through the dashboard could never have worked (the test harness wired it correctly — the gap was production-only and latent, because nothing in the dashboard evaluated trees before this migration). The dashboard handler and both inline sites now receive the search service, and evaluation failures log instead of silently emptying a section. Supersedes the same-day loud-400 stance for the builder's payload (contract_pins migration entry); custom-section-builder.md documents the save contract; the create_collection API doc gains the adapted fields. New TestCustomSectionBuilderSave/BadRule pin the payload → tree → dashboard-render path end to end. Full container suite green (155s).
166 lines
4.3 KiB
Markdown
166 lines
4.3 KiB
Markdown
# Custom Section Builder API
|
|
|
|
The Custom Section Builder allows users to create personalized dashboard sections by defining filter rules or manually selecting books.
|
|
|
|
## Preview Collection
|
|
|
|
Evaluates filter rules and returns matching items without saving the collection.
|
|
|
|
**Endpoint:** `POST /api/collections/preview`
|
|
|
|
**Request Body:**
|
|
|
|
```json
|
|
{
|
|
"library_id": "uuid",
|
|
"rules": [
|
|
{
|
|
"id": "rule1",
|
|
"field": "genre",
|
|
"operator": "equals",
|
|
"value": "Sci-Fi",
|
|
"priority": 1
|
|
}
|
|
],
|
|
"manual_book_ids": ["uuid1", "uuid2"],
|
|
"limit": 20
|
|
}
|
|
```
|
|
|
|
**Available Filter Fields:**
|
|
|
|
| Field | Type | Operators |
|
|
| ------------ | ------ | ------------------------------------------------------------------------ |
|
|
| `title` | text | contains, equals, starts_with, ends_with, regex |
|
|
| `author` | text | contains, equals |
|
|
| `genre` | select | equals, not_equals, in, not_in |
|
|
| `series` | text | is_set, is_not_set, equals, contains |
|
|
| `progress` | number | equals, not_equals, greater_than, less_than, between, is_set, is_not_set |
|
|
| `rating` | number | equals, not_equals, greater_than, less_than, is_set, is_not_set |
|
|
| `date_added` | date | equals, not_equals, before, after, between, last_x_days |
|
|
| `last_read` | date | equals, before, after, between, last_x_days, is_set, is_not_set |
|
|
| `publisher` | text | contains, equals |
|
|
| `language` | select | equals, not_equals, in |
|
|
| `format` | select | equals, in |
|
|
| `tags` | text | contains, not_contains, equals |
|
|
| `narrators` | text | contains, equals, is_set, is_not_set |
|
|
|
|
**Response:**
|
|
|
|
```json
|
|
{
|
|
"items": [
|
|
{
|
|
"media_item_id": "uuid",
|
|
"title": "Book Title",
|
|
"author": "Author Name",
|
|
"cover_image_path": "/path/to/cover.jpg"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
## Create Custom Section
|
|
|
|
Creates a new custom collection with filter rules and/or manual book selection.
|
|
|
|
**Endpoint:** `POST /api/collections`
|
|
|
|
**Request Body:**
|
|
|
|
**Save contract (2026-10-31 tree migration):** the rules array travels
|
|
as a JSON-encoded string and the server converts it to a filter tree at
|
|
write time — `match_type` seeds the tree's root mode (`any` default),
|
|
`manual_book_ids` attach to the created collection, and
|
|
`show_on_dashboard` persists. `library_id` is selection context only
|
|
(sections scope to the dashboard's active library at render). Invalid
|
|
rules are a 400 at write naming the problem; the section renders stored
|
|
picks + the tree's live matches, deduped.
|
|
|
|
```json
|
|
{
|
|
"name": "My Custom Section",
|
|
"description": "My favorite Sci-Fi books",
|
|
"icon": "📚",
|
|
"color": "#9333ea",
|
|
"show_on_dashboard": true,
|
|
"auto_assign_rules": "[{\"id\":\"rule1\",\"field\":\"genre\",\"operator\":\"equals\",\"value\":\"Sci-Fi\",\"priority\":1}]",
|
|
"manual_book_ids": ["uuid1", "uuid2"],
|
|
"view_settings": {}
|
|
}
|
|
```
|
|
|
|
**Response:** Returns the created collection object.
|
|
|
|
## Frontend Implementation
|
|
|
|
**Route:** `/custom-section`
|
|
|
|
**Template:** `templates/custom_section.templ`
|
|
|
|
**TypeScript:** `web/src/custom-section-builder.ts`
|
|
|
|
Key features:
|
|
|
|
- 14 filter fields with various operators
|
|
- Live preview functionality
|
|
- Search + multi-select for manual book addition
|
|
- AND/OR logic support for combining rules
|
|
|
|
## Example Use Cases
|
|
|
|
### Sci-Fi Favorites
|
|
|
|
```json
|
|
{
|
|
"rules": [
|
|
{
|
|
"field": "genre",
|
|
"operator": "equals",
|
|
"value": "Sci-Fi"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### High Rated Books
|
|
|
|
```json
|
|
{
|
|
"rules": [
|
|
{
|
|
"field": "rating",
|
|
"operator": "greater_than",
|
|
"value": "4"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Long Books (Manual Selection)
|
|
|
|
```json
|
|
{
|
|
"manual_book_ids": ["uuid1", "uuid2", "uuid3"]
|
|
}
|
|
```
|
|
|
|
### Recently Finished Audiobooks
|
|
|
|
```json
|
|
{
|
|
"rules": [
|
|
{
|
|
"field": "format",
|
|
"operator": "equals",
|
|
"value": "Audiobook"
|
|
},
|
|
{
|
|
"field": "last_read",
|
|
"operator": "last_x_days",
|
|
"value": "30"
|
|
}
|
|
]
|
|
}
|
|
```
|