Files
bookhoard/docs/developer/api/custom-section-builder.md
T
John O'Keefe a86bdac87e refactor(collections): retire the legacy rules surface — tree-only storage
The pre-tree rules machinery is deleted: no row ever carried a rules
array or a non-default match mode (verified in the dev DB), the web
never read the envelope keys, and the Android app consumes none of it.
Dead code gets no documentation.

Removed:
- PUT /api/collections/:id/match-mode — endpoint, route, and the
  UpdateCollectionMatchMode query. The tree's root mode owns any|all.
- auto_assign_rules / rule_match_mode from every envelope
  (create/list/detail/update), from the create/update request shapes,
  and from the schema: both columns DROPPED (schema.sql carries
  idempotent DROP COLUMN IF EXISTS heals for existing DBs; sqlc
  regenerated).
- The rules CRUD service layer (AddRule/SetRuleEnabled/RemoveRule +
  validateRule + UpdateCollectionRulesJSON), ParseRules /
  EnabledRules / IsSmartCollection, RulesToFilterTree (+ the SSR
  preload branch), the legacy branch of EvaluateSmartMembership, and
  SmartEvalOptions.MatchMode. IsCollectionSmart = tree presence;
  create-path isSmart = collection_type=smart OR a filter_tree.

Kept (live consumers): POST /api/collections/preview still evaluates
rules payloads in memory (the custom-section builder's preview), and
include_auto stays on the list (the Android app sends it).

The custom-section builder's SAVE was already broken — its
stringified auto_assign_rules payload failed request binding (probed
live: 400 invalid request). Create now rejects that payload loudly
with a 400 naming the retirement, instead of silently creating a
hollow static section; custom-section-builder.md carries the flag.
The builder's tree migration remains the flagged follow-up.

Test-side (asymmetry rule — deleted behavior, deleted tests):
TestPodcastSmartCollections rewritten onto trees (mode lives in the
tree root), match-mode subtests and tree_wins_over_legacy_rules
deleted, TestRulesToFilterTree deleted, contract-pin envelope keys
retracted per the contract_pins 2026-10-31 retirement entry. Mirrors
hand-synced: openapi.yaml, bruno create/update, docs/api/*.md,
docs/schema.go, api.d.ts. Full container suite green (155s).
2026-10-31 19:06:48 -04:00

4.1 KiB

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:

{
  "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:

{
  "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:

Status (2026-10-31): the rules-array storage this payload targets was retired — collections carry filter trees now. The server rejects this save with a 400 naming the retired fields; the builder needs its flagged tree migration (§12) before saving works again.

{
  "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

{
  "rules": [
    {
      "field": "genre",
      "operator": "equals",
      "value": "Sci-Fi"
    }
  ]
}

High Rated Books

{
  "rules": [
    {
      "field": "rating",
      "operator": "greater_than",
      "value": "4"
    }
  ]
}

Long Books (Manual Selection)

{
  "manual_book_ids": ["uuid1", "uuid2", "uuid3"]
}

Recently Finished Audiobooks

{
  "rules": [
    {
      "field": "format",
      "operator": "equals",
      "value": "Audiobook"
    },
    {
      "field": "last_read",
      "operator": "last_x_days",
      "value": "30"
    }
  ]
}