diff --git a/docs/developer/api/dashboard.md b/docs/developer/api/dashboard.md deleted file mode 100644 index 0c0cfdc3..00000000 --- a/docs/developer/api/dashboard.md +++ /dev/null @@ -1,143 +0,0 @@ -# Dashboard API - -## Get Dashboard Sections - -Retrieve all dashboard sections for a specific library, including system collections and user collections. - -**Endpoint**: `GET /api/dashboard/sections` - -**Authentication**: Required (Bearer token) - -### Query Parameters - -| Parameter | Type | Required | Description | -| ---------- | ------ | -------- | ----------------------------------------- | -| library_id | string | Yes | Library UUID to fetch sections for | -| limit | number | No | Items per section (default: 20, max: 100) | - -### Response - -Returns array of sections in user's customized order (respects `collection_order` and `hidden_collections` preferences). - -**Section Types**: - -- `is_system: true`: System collections (4 pre-seeded defaults) -- `is_system: false`: User-created collections with `show_on_dashboard: true` - -**System Collections**: -| ID | Title | Icon | Description | -|-----------------|------------------|------|--------------------------------------------------| -| continue-reading| Continue Reading | 📖 | Books with progress > 0% and < 100% | -| recently-added | Recently Added | 🆕 | Newest items in library | -| recently-read | Recently Read | ✅ | Books with progress = 100% | -| not-started | Not Started | 📕 | Books with no reading progress | - -### Example Response - -```json -{ - "sections": [ - { - "id": "continue-reading", - "is_system": true, - "title": "Continue Reading", - "description": "Books you're currently reading (0 < progress < 1)", - "icon": "📖", - "items": [ - { - "media_item_id": "uuid-here", - "title": "Book Title", - "author": "Author Name", - "cover_image_path": "/path/to/cover.jpg" - } - ], - "view_all_url": "/section/continue-reading", - "priority": 1 - }, - { - "id": "collection-uuid", - "is_system": false, - "title": "My Favorites", - "description": "My favorite books", - "icon": "⭐", - "items": [], - "view_all_url": "", - "priority": 100 - } - ] -} -``` - -### User Preferences - -The endpoint respects user's dashboard preferences: - -- **`collection_order`**: Sections returned in user's custom order -- **`hidden_collections`**: Hidden collections excluded from response -- **`items_per_section`**: Default limit from user preferences (overridden by `?limit=` query param) - -## Update Dashboard Preferences - -Save or update user dashboard preferences for a specific library. - -**Endpoint**: `PUT /api/dashboard/preferences` - -**Authentication**: Required (Bearer token) - -### Request Body - -```json -{ - "library_id": "uuid", - "hidden_collections": ["not-started"], - "collection_order": ["recently-added", "continue-reading", "recently-read"], - "items_per_section": 20 -} -``` - -### Response - -Returns updated preferences object. - -## Restore System Collection - -Reset a system collection to its default state (removes user customizations). - -**Endpoint**: `POST /api/dashboard/restore-system-collection` - -**Authentication**: Required (Bearer token) - -### Request Body - -```json -{ - "collection_name": "continue-reading" -} -``` - -Valid `collection_name` values: - -- `continue-reading` -- `recently-added` -- `recently-read` -- `not-started` - -### Response - -```json -{ - "message": "System collection restored to defaults" -} -``` - -### Error Responses - -| Status | Description | -| ------ | ---------------------------- | -| 400 | Missing library_id | -| 400 | Invalid library_id | -| 400 | Invalid collection_name | -| 401 | Unauthorized | -| 500 | Failed to load sections | -| 500 | Failed to save preferences | -| 500 | Failed to restore collection | diff --git a/docs/developer/api/dashboard/get_preferences.md b/docs/developer/api/dashboard/get_preferences.md new file mode 100644 index 00000000..b4905cb4 --- /dev/null +++ b/docs/developer/api/dashboard/get_preferences.md @@ -0,0 +1,34 @@ +--- +method: GET +path: /api/dashboard/preferences +auth: jwt +area: dashboard +--- + + +# Get Dashboard Preferences + +Returns the signed-in user's dashboard preferences for a library (or the +global scope when `library_id` is omitted). + +## Example response + +```json example-response +{ + "hidden_collections": [], + "collection_order": [], + "items_per_section": 20 +} +``` + +## Query parameters + +| Param | Type | Required | Notes | +| ----- | ---- | -------- | ----- | +| library_id | uuid | No | Library scope; omitted = global | + +## Errors + +| Status | Body | When | +| ------ | ---- | ---- | +| 400 | `{"error": "invalid library_id"}` | malformed library_id | \ No newline at end of file diff --git a/docs/developer/api/dashboard/get_sections.md b/docs/developer/api/dashboard/get_sections.md new file mode 100644 index 00000000..ad3aeb56 --- /dev/null +++ b/docs/developer/api/dashboard/get_sections.md @@ -0,0 +1,49 @@ +--- +method: GET +path: /api/dashboard/sections +auth: jwt +area: dashboard +--- + + +# Get Dashboard Sections + +Returns all dashboard sections for a library, in the user's customized order. +Respects `collection_order` and `hidden_collections` preferences. `library_id` +is optional; when omitted the global (all-libraries) scope is used. + +## Example response + +```json example-response +{ + "sections": [ + { + "id": "continue-reading", + "collection_id": "", + "is_system": true, + "title": "Continue Reading", + "description": "Books you're currently reading (0 < progress < 1)", + "icon": "📖", + "items": [], + "view_all_url": "", + "priority": 1, + "is_playlist": false, + "complete": false + } + ] +} +``` + +## Query parameters + +| Param | Type | Required | Notes | +| ----- | ---- | -------- | ----- | +| library_id | uuid | No | Library to scope sections to | +| limit | int | No | Items per section (1-100, default from preferences) | + +## Errors + +| Status | Body | When | +| ------ | ---- | ---- | +| 400 | `{"error": "invalid library_id"}` | malformed library_id | +| 500 | `{"error": "Failed to load dashboard sections"}` | unexpected server error | \ No newline at end of file diff --git a/docs/developer/api/dashboard/restore_system_collection.md b/docs/developer/api/dashboard/restore_system_collection.md new file mode 100644 index 00000000..6c0c3649 --- /dev/null +++ b/docs/developer/api/dashboard/restore_system_collection.md @@ -0,0 +1,42 @@ +--- +method: POST +path: /api/dashboard/restore-system-collection +auth: jwt +area: dashboard +--- + + +# Restore System Collection + +Resets a system collection to its default state, removing user customizations. + +## Example request + +```json example-request +{ + "collection_name": "Continue Reading", + "reset_type": "full" +} +``` + +## Example response + +```json example-response +{ + "message": "System collection restored to defaults" +} +``` + +## Fields + +| Field | Type | Required | Notes | +| ----- | ---- | -------- | ----- | +| collection_name | string | Yes | One of `Continue Reading`, `Recently Added`, `Recently Read`, `Not Started`, `Continue Series` | +| reset_type | string | No | `full` (default) or `keep_books` | + +## Errors + +| Status | Body | When | +| ------ | ---- | ---- | +| 400 | `{"error": "Invalid request"}` / `{"error": "collection_name required"}` / `{"error": "Invalid system collection name"}` / `{"error": "reset_type must be 'full' or 'keep_books'"}` | malformed body or invalid values | +| 500 | `{"error": "Failed to restore system collection"}` | unexpected server error | \ No newline at end of file diff --git a/docs/developer/api/dashboard/update_preferences.md b/docs/developer/api/dashboard/update_preferences.md new file mode 100644 index 00000000..5ed75837 --- /dev/null +++ b/docs/developer/api/dashboard/update_preferences.md @@ -0,0 +1,53 @@ +--- +method: PUT +path: /api/dashboard/preferences +auth: jwt +area: dashboard +--- + + +# Update Dashboard Preferences + +Saves or updates the signed-in user's dashboard preferences for a library. + +## Example request + +```json example-request +{ + "library_id": "550e8400-e29b-41d4-a716-446655440000", + "hidden_collections": ["not-started"], + "collection_order": ["recently-added", "continue-reading", "recently-read"], + "items_per_section": 20 +} +``` + +## Example response + +```json example-response +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "user_id": "550e8400-e29b-41d4-a716-446655440000", + "library_id": "550e8400-e29b-41d4-a716-446655440000", + "hidden_collections": ["not-started"], + "collection_order": ["recently-added", "continue-reading", "recently-read"], + "items_per_section": 20, + "created_at": "2026-10-24T13:57:01Z", + "updated_at": "2026-10-24T13:57:01Z" +} +``` + +## Fields + +| Field | Type | Required | Notes | +| ----- | ---- | -------- | ----- | +| library_id | string (uuid) | No | `null`/absent = global scope; empty string is rejected | +| hidden_collections | array | No | Collection ids to hide | +| collection_order | array | No | Ordered list of collection ids | +| items_per_section | int | No | Items per section | + +## Errors + +| Status | Body | When | +| ------ | ---- | ---- | +| 400 | `{"error": "Invalid request"}` / `{"error": "invalid library_id"}` | malformed body or library_id | +| 500 | `{"error": "Failed to save preferences"}` | unexpected server error | \ No newline at end of file diff --git a/docs/developer/api/saved-filters/create_saved_filter.md b/docs/developer/api/saved-filters/create_saved_filter.md new file mode 100644 index 00000000..158b4d6c --- /dev/null +++ b/docs/developer/api/saved-filters/create_saved_filter.md @@ -0,0 +1,50 @@ +--- +method: POST +path: /api/saved-filters +auth: jwt +area: saved-filters +--- + + +# Create Saved Filter + +Creates a new saved filter for the signed-in user. + +## Example request + +```json example-request +{ + "name": "My Filter", + "resource_type": "media-items", + "filters": {} +} +``` + +## Example response + +```json example-response +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "My Filter", + "resource_type": "media-items", + "filters": {}, + "created_at": "2026-10-24T13:57:01Z", + "updated_at": "2026-10-24T13:57:01Z" +} +``` + +## Fields + +| Field | Type | Required | Notes | +| ----- | ---- | -------- | ----- | +| name | string | Yes | 1-100 chars | +| resource_type | string | Yes | One of `media-items`, `collections`, `devices` | +| filters | object | Yes | Map of filter key/value pairs | + +## Errors + +| Status | Body | When | +| ------ | ---- | ---- | +| 400 | `{"error": "invalid request body"}` / `{"error": "name and resource_type are required"}` | malformed body or missing fields | +| 409 | `{"error": "filter with name '...' already exists for this resource type"}` | duplicate name for that resource type | +| 500 | `{"error": "failed to create filter"}` | unexpected server error | \ No newline at end of file diff --git a/docs/developer/api/saved-filters/delete_saved_filter.md b/docs/developer/api/saved-filters/delete_saved_filter.md new file mode 100644 index 00000000..5fffd5f8 --- /dev/null +++ b/docs/developer/api/saved-filters/delete_saved_filter.md @@ -0,0 +1,21 @@ +--- +method: DELETE +path: /api/saved-filters/:id +auth: jwt +area: saved-filters +--- + + +# Delete Saved Filter + +Deletes a saved filter owned by the signed-in user. + +## Errors + +| Status | Body | When | +| ------ | ---- | ---- | +| 400 | `{"error": "invalid filter ID"}` | malformed id | +| 404 | `{"error": "filter not found"}` | no such filter (or not owned) | +| 500 | `{"error": "failed to delete filter"}` | unexpected server error | + +Returns `204 No Content` on success. \ No newline at end of file diff --git a/docs/developer/api/saved-filters/get_saved_filter.md b/docs/developer/api/saved-filters/get_saved_filter.md new file mode 100644 index 00000000..73813336 --- /dev/null +++ b/docs/developer/api/saved-filters/get_saved_filter.md @@ -0,0 +1,32 @@ +--- +method: GET +path: /api/saved-filters/:id +auth: jwt +area: saved-filters +--- + + +# Get Saved Filter + +Returns one saved filter owned by the signed-in user. + +## Example response + +```json example-response +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "My Filter", + "resource_type": "media-items", + "filters": {}, + "created_at": "2026-10-24T13:57:01Z", + "updated_at": "2026-10-24T13:57:01Z" +} +``` + +## Errors + +| Status | Body | When | +| ------ | ---- | ---- | +| 400 | `{"error": "invalid filter ID"}` | malformed id | +| 404 | `{"error": "filter not found"}` | no such filter (or not owned) | +| 500 | `{"error": "failed to fetch filter"}` | unexpected server error | \ No newline at end of file diff --git a/docs/developer/api/saved-filters/list_saved_filters.md b/docs/developer/api/saved-filters/list_saved_filters.md new file mode 100644 index 00000000..237462f2 --- /dev/null +++ b/docs/developer/api/saved-filters/list_saved_filters.md @@ -0,0 +1,40 @@ +--- +method: GET +path: /api/saved-filters +auth: jwt +area: saved-filters +--- + + +# List Saved Filters + +Returns the signed-in user's saved filters for a given resource type. The +`resource_type` query parameter is required. + +## Example response + +```json example-response +[ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "My Filter", + "resource_type": "media-items", + "filters": {}, + "created_at": "2026-10-24T13:57:01Z", + "updated_at": "2026-10-24T13:57:01Z" + } +] +``` + +## Query parameters + +| Param | Type | Required | Notes | +| ----- | ---- | -------- | ----- | +| resource_type | string | Yes | One of `media-items`, `collections`, `devices` | + +## Errors + +| Status | Body | When | +| ------ | ---- | ---- | +| 400 | `{"error": "resource_type query parameter is required"}` | missing `resource_type` | +| 500 | `{"error": "failed to fetch filters"}` | unexpected server error | \ No newline at end of file diff --git a/docs/developer/api/saved-filters/update_saved_filter.md b/docs/developer/api/saved-filters/update_saved_filter.md new file mode 100644 index 00000000..38dc6837 --- /dev/null +++ b/docs/developer/api/saved-filters/update_saved_filter.md @@ -0,0 +1,50 @@ +--- +method: PUT +path: /api/saved-filters/:id +auth: jwt +area: saved-filters +--- + + +# Update Saved Filter + +Updates an existing saved filter owned by the signed-in user. + +## Example request + +```json example-request +{ + "name": "Renamed Filter", + "resource_type": "media-items", + "filters": {} +} +``` + +## Example response + +```json example-response +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Renamed Filter", + "resource_type": "media-items", + "filters": {}, + "created_at": "2026-10-24T13:57:01Z", + "updated_at": "2026-10-24T13:57:01Z" +} +``` + +## Fields + +| Field | Type | Required | Notes | +| ----- | ---- | -------- | ----- | +| name | string | Yes | 1-100 chars | +| resource_type | string | Yes | One of `media-items`, `collections`, `devices` | +| filters | object | Yes | Map of filter key/value pairs | + +## Errors + +| Status | Body | When | +| ------ | ---- | ---- | +| 400 | `{"error": "invalid filter ID"}` / `{"error": "invalid request body"}` | malformed id or body | +| 409 | `{"error": "filter with name '...' already exists for this resource type"}` | duplicate name | +| 500 | `{"error": "failed to update filter"}` | unexpected server error | \ No newline at end of file diff --git a/docs/developer/api/series/get_series_books.md b/docs/developer/api/series/get_series_books.md new file mode 100644 index 00000000..f0a79a03 --- /dev/null +++ b/docs/developer/api/series/get_series_books.md @@ -0,0 +1,47 @@ +--- +method: GET +path: /api/series/books +auth: jwt +area: series +--- + + +# Get Series Books + +Returns the books belonging to a named series within a library. + +## Example response + +```json example-response +{ + "name": "The Witcher", + "library_id": "550e8400-e29b-41d4-a716-446655440000", + "books": [ + { + "media_item_id": "550e8400-e29b-41d4-a716-446655440000", + "title": "The Last Wish", + "author": "Andrzej Sapkowski", + "cover_image_path": "", + "has_conflict": false, + "format_group": "ebook", + "badge": "" + } + ], + "total": 1 +} +``` + +## Query parameters + +| Param | Type | Required | Notes | +| ----- | ---- | -------- | ----- | +| name | string | Yes | Series name | +| library_id | uuid | Yes | Library containing the series (must be visible) | + +## Errors + +| Status | Body | When | +| ------ | ---- | ---- | +| 400 | `{"error": "name required"}` / `{"error": "library_id required"}` / `{"error": "invalid library_id"}` | missing or malformed params | +| 403 | `{"error": "library not accessible"}` | library not in visible set | +| 500 | `{"error": "Failed to load series books"}` | unexpected server error | \ No newline at end of file diff --git a/docs/developer/api/series/list_series.md b/docs/developer/api/series/list_series.md new file mode 100644 index 00000000..dba76531 --- /dev/null +++ b/docs/developer/api/series/list_series.md @@ -0,0 +1,47 @@ +--- +method: GET +path: /api/series +auth: jwt +area: series +--- + + +# List Series + +Returns a paginated list of series for the requesting user's visible libraries. + +## Example response + +```json example-response +{ + "series": [ + { + "name": "The Witcher", + "library_id": "550e8400-e29b-41d4-a716-446655440000", + "book_count": 5, + "total_in_series": 8, + "cover_paths": [], + "last_entry_at": "2026-10-24T13:57:01Z" + } + ], + "total": 1, + "limit": 20, + "offset": 0 +} +``` + +## Query parameters + +| Param | Type | Required | Notes | +| ----- | ---- | -------- | ----- | +| library_id | uuid | No | Scope to one library (must be visible) | +| limit | int | No | 1-100 (default 20) | +| offset | int | No | Pagination offset | + +## Errors + +| Status | Body | When | +| ------ | ---- | ---- | +| 400 | `{"error": "invalid library_id"}` | malformed library_id | +| 403 | `{"error": "library not accessible"}` | library not in visible set | +| 500 | `{"error": "Failed to load series"}` | unexpected server error | \ No newline at end of file diff --git a/docs/developer/api/system/config.md b/docs/developer/api/system/config.md deleted file mode 100644 index 9ba8fe99..00000000 --- a/docs/developer/api/system/config.md +++ /dev/null @@ -1,77 +0,0 @@ -# System Config API - -## Overview - -Raw key/value system configuration storage (backed by the `system_config` table). Unlike the typed [System Settings API](settings.md), this endpoint reads and writes arbitrary config keys as plain strings — including keys without registry metadata, such as `base_url`. - -**Base URL**: `/api/system` -**Authentication**: Admin JWT token required -**Content-Type**: `application/json` - ---- - -## Endpoints - -### Get System Configuration - -Retrieve all system configuration entries as a flat key/value map. - -**Endpoint**: `GET /api/system/config` - -**Authentication**: Admin role required - -**Response**: **200 OK** - -```json -{ - "base_url": "http://192.168.1.100:8765", - "default_timezone": "America/New_York" -} -``` - -**Example**: - -```bash -curl -X GET https://bookhoard.example.com/api/system/config \ - -H "Authorization: Bearer " -``` - ---- - -### Update System Configuration - -Update one or more config values. - -**Endpoint**: `PUT /api/system/config` - -**Authentication**: Admin role required - -**Request Body**: a flat map of keys to string values. Only the supplied keys are updated. - -```json -{ - "base_url": "https://bookhoard.example.com" -} -``` - -**Validation**: values for known keys are validated where applicable — for example, `default_timezone` must be a valid IANA timezone (`time.LoadLocation`); invalid values return `400` without persisting. - -**Response**: **200 OK** on success; `400` (invalid value/format), `401`, `403`, `500` on failure. - -**Example**: - -```bash -curl -X PUT https://bookhoard.example.com/api/system/config \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{"base_url": "https://bookhoard.example.com"}' -``` - -> **Note:** settings that appear in the typed settings registry (e.g. `default_timezone`) are better managed through [`PUT /api/system/settings`](settings.md), which also returns metadata and reload hints. Writes through either endpoint refresh the shared registry cache. - ---- - -## Related Endpoints - -- [System Settings API](settings.md) — typed, validated tunable settings with metadata -- `GET /api/devices/:id/sidecar` — device setup config derived from system config (see [Devices API](../devices/)) diff --git a/docs/developer/api/system/get_config.md b/docs/developer/api/system/get_config.md new file mode 100644 index 00000000..56f4c28b --- /dev/null +++ b/docs/developer/api/system/get_config.md @@ -0,0 +1,30 @@ +--- +method: GET +path: /api/system/config +auth: jwt+admin +area: system +--- + + +# Get System Configuration + +Returns all system-wide configuration key/value pairs (backed by the +`system_config` table). + +## Example response + +```json example-response +{ + "base_url": "https://books.example.com", + "opds_base_url": "https://books.example.com/opds", + "api_base_url": "https://books.example.com/api" +} +``` + +UNVERIFIED: exact config key set is arbitrary (admin-defined); example shows common keys only. + +## Errors + +| Status | Body | When | +| ------ | ---- | ---- | +| 500 | `{"error": "failed to fetch system config"}` | unexpected server error | \ No newline at end of file diff --git a/docs/developer/api/system/get_settings.md b/docs/developer/api/system/get_settings.md new file mode 100644 index 00000000..1610be8d --- /dev/null +++ b/docs/developer/api/system/get_settings.md @@ -0,0 +1,34 @@ +--- +method: GET +path: /api/system/settings +auth: jwt+admin +area: system +--- + + +# Get System Settings + +Returns metadata and current values for every known tunable system setting. + +## Example response + +```json example-response +[ + { + "key": "scan_poll_interval_seconds", + "value": "60", + "type": "int", + "requires_restart": false, + "category": "scanner", + "group": "Scanning", + "description": "How often to scan all libraries (seconds)", + "is_default": true + } +] +``` + +## Errors + +| Status | Body | When | +| ------ | ---- | ---- | +| 503 | `{"error": "settings registry not initialized"}` | registry not wired | \ No newline at end of file diff --git a/docs/developer/api/system/settings.md b/docs/developer/api/system/settings.md deleted file mode 100644 index f39169a3..00000000 --- a/docs/developer/api/system/settings.md +++ /dev/null @@ -1,196 +0,0 @@ -# System Settings API - -## Overview - -The System Settings API is the canonical way to read and write Bookhoard's tunable system settings (scanning, security, rate limits, sync, performance, and defaults). Every setting carries full metadata — type, range, category, description, and whether a restart is required — so the admin UI (and API clients) can render and validate settings generically. - -**Base URL**: `/api/system` -**Authentication**: Admin JWT token required -**Content-Type**: `application/json` - ---- - -## Endpoints - -### List All Settings - -Retrieve every known tunable setting with its current value and metadata. - -**Endpoint**: `GET /api/system/settings` - -**Authentication**: Admin role required - -**Response**: **200 OK** - -```json -[ - { - "key": "scan_poll_interval_seconds", - "value": "60", - "type": "int", - "min": "1", - "max": "3600", - "requires_restart": false, - "category": "scanner", - "group": "Scanning", - "description": "How often to scan all libraries (seconds)", - "is_default": true - } -] -``` - -**Entry fields**: - -| Field | Type | Description | -| ------------------ | ------- | -------------------------------------------------------- | -| `key` | string | Setting identifier (stable API name) | -| `value` | string | Current value (validated/clamped by the registry) | -| `type` | string | `int`, `bool`, or `string` | -| `min` / `max` | string | Range bounds for `int` settings (omitted otherwise) | -| `requires_restart` | boolean | Change takes effect only after a server restart | -| `category` | string | Coarse area: `scanner`, `security`, `api`, `sync`, `performance`, `general` | -| `group` | string | Sub-section shown in the admin UI | -| `description` | string | Human-readable description | -| `is_default` | boolean | True when the current value equals the compiled default | - -**Example**: - -```bash -curl -X GET https://bookhoard.example.com/api/system/settings \ - -H "Authorization: Bearer " -``` - ---- - -### Update a Setting - -Validate, persist, and reload a single setting. - -**Endpoint**: `PUT /api/system/settings` - -**Authentication**: Admin role required - -**Request Body**: - -```json -{ - "key": "scan_poll_interval_seconds", - "value": "30" -} -``` - -| Field | Type | Required | Description | -| ------- | ------ | -------- | ------------------------------- | -| `key` | string | Yes | Setting key (from the list) | -| `value` | string | Yes | New value, as a string | - -**Response**: **200 OK** - -```json -{ - "key": "scan_poll_interval_seconds", - "value": "30", - "type": "int", - "min": "1", - "max": "3600", - "requires_restart": false, - "category": "scanner", - "group": "Scanning", - "description": "How often to scan all libraries (seconds)", - "is_default": false, - "reload_required": false, - "message": "" -} -``` - -- `reload_required: true` means the change takes effect only after a restart (e.g. rate limits, worker pool, lockout settings). -- Validation is type-aware: `int` values are checked against `min`/`max`, `bool` values must parse, `default_timezone` must be a valid IANA timezone via `time.LoadLocation`, and strings must be non-empty. - -**Errors**: `400` (unknown key, invalid value, out of range), `401`, `403`, `503` (settings registry not initialized). - -**Example**: - -```bash -curl -X PUT https://bookhoard.example.com/api/system/settings \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{"key": "scan_poll_interval_seconds", "value": "30"}' -``` - ---- - -## Setting Catalog - -Current tunable settings by category: - -**Scanner** (`scanner`) - -| Key | Default | Range | Restart | Description | -| ------------------------------ | ------- | -------- | ------- | ----------------------------------------- | -| `scan_poll_interval_seconds` | `60` | 1-3600 | No | How often to scan all libraries (seconds) | -| `auto_scan_enabled` | `true` | - | No | Whether auto-scanning is enabled | - -**General** (`general`) - -| Key | Default | Restart | Description | -| ----------------- | ------- | ------- | ------------------------- | -| `default_timezone`| `UTC` | No | System default timezone | - -**Security** (`security`) - -| Key | Default | Range | Restart | Description | -| ---------------------------- | --------- | ------------ | ------- | ---------------------------------------------- | -| `session_duration_seconds` | `604800` | 300-31536000 | No | How long a login session stays valid | -| `password_min_length` | `8` | 1-128 | No | Minimum password length | -| `password_require_upper` | `true` | - | No | Require at least one uppercase letter | -| `password_require_lower` | `true` | - | No | Require at least one lowercase letter | -| `password_require_number` | `true` | - | No | Require at least one number | -| `password_require_special` | `true` | - | No | Require at least one special character | -| `auth_rate_limit_per_min` | `10` | 1-10000 | **Yes** | Global auth API rate limit (req/min) | -| `login_max_attempts` | `5` | 1-100 | **Yes** | Failed login attempts before lockout | -| `login_lockout_minutes` | `15` | 1-10080 | **Yes** | Lockout duration after failed logins | - -**API** (`api`) - -| Key | Default | Range | Restart | Description | -| ------------------------------- | ------- | --------- | ------- | ------------------------------------ | -| `opds_default_page_size` | `50` | 1-500 | No | Default OPDS page size | -| `opds_max_page_size` | `200` | 1-1000 | No | Maximum OPDS page size | -| `device_rate_sync_per_min` | `60` | 1-10000 | No | Device sync requests per minute | -| `device_rate_progress_per_min` | `120` | 1-10000 | No | Device progress requests per minute | -| `device_rate_metadata_per_min` | `30` | 1-10000 | No | Device metadata requests per minute | - -**Sync** (`sync`) - -| Key | Default | Range | Restart | Description | -| ------------------------------- | ------- | -------- | ------- | -------------------------------------------------- | -| `annotation_tombstone_ttl_days` | `30` | 1-3650 | No | How long deleted annotations are kept before purge | -| `sync_queue_interval_seconds` | `5` | 1-3600 | **Yes** | How often the sync queue flushes | -| `sync_queue_batch_size` | `50` | 1-10000 | **Yes** | Max items processed per sync queue flush | - -**Performance** (`performance`) - -| Key | Default | Range | Restart | Description | -| ------------------------ | ------- | --------- | ------- | ------------------------------------------- | -| `conversion_cache_ttl_hours` | `24` | 1-720 | No | How long converted (KEPUB) files are cached | -| `worker_pool_size` | `3` | 1-100 | **Yes** | Number of background worker goroutines | -| `worker_queue_cap` | `100` | 1-10000 | **Yes** | Background worker job queue capacity | - ---- - -## Legacy Scan Settings Routes - -The older JSON routes still work for backward compatibility and now refresh the settings registry cache on write, but they are **superseded** by `GET/PUT /api/system/settings`: - -- `GET /api/libraries/scan-settings` — returns only `scan_poll_interval_seconds` and `auto_scan_enabled` -- `PUT /api/libraries/scan-settings` — accepts `{ "scan_poll_interval_seconds": int, "auto_scan_enabled": bool }` - -Both fields are backed by the same registry entries documented above. - ---- - -## Related Endpoints - -- `GET/PUT /api/system/config` — raw key/value system configuration (see [System Config API](config.md)) -- `POST /api/scanner/scan` — trigger a manual scan (see [Scanner API](../scanner/)) -- `GET /api/admin/hash-conflicts` — duplicates found during hashing (see [Hash Conflicts API](../admin/hash-conflicts.md)) diff --git a/docs/developer/api/system/update_config.md b/docs/developer/api/system/update_config.md new file mode 100644 index 00000000..91d1fbd1 --- /dev/null +++ b/docs/developer/api/system/update_config.md @@ -0,0 +1,46 @@ +--- +method: PUT +path: /api/system/config +auth: jwt+admin +area: system +--- + + +# Update System Configuration + +Updates one or more system-wide configuration keys. Admin only. When +`base_url` is provided, derived keys (`opds_base_url`, `api_base_url`) are +updated automatically. `default_timezone` is validated as a timezone. + +## Example request + +```json example-request +{ + "base_url": "https://books.example.com", + "default_timezone": "UTC" +} +``` + +## Example response + +```json example-response +{ + "status": "success", + "message": "System configuration updated" +} +``` + +## Fields + +| Field | Type | Required | Notes | +| ----- | ---- | -------- | ----- | +| (any key) | string | No | Arbitrary config key/value pairs | + +## Errors + +| Status | Body | When | +| ------ | ---- | ---- | +| 400 | `{"error": "invalid request format"}` / `{"error": "invalid timezone"}` | malformed body or bad timezone | +| 401 | `{"error": "unauthorized"}` | no/invalid JWT | +| 403 | `{"error": "admin access required"}` | non-admin | +| 500 | `{"error": "failed to update config key: ..."}` / `{"error": "failed to update default timezone"}` / `{"error": "failed to update derived config key: ..."}` | unexpected server error | \ No newline at end of file diff --git a/docs/developer/api/system/update_setting.md b/docs/developer/api/system/update_setting.md new file mode 100644 index 00000000..1fc2d3e9 --- /dev/null +++ b/docs/developer/api/system/update_setting.md @@ -0,0 +1,50 @@ +--- +method: PUT +path: /api/system/settings +auth: jwt+admin +area: system +--- + + +# Update System Setting + +Validates, persists, and reloads a single tunable system setting. Admin only. + +## Example request + +```json example-request +{ + "key": "scan_poll_interval_seconds", + "value": "60" +} +``` + +## Example response + +```json example-response +{ + "key": "scan_poll_interval_seconds", + "value": "60", + "type": "int", + "requires_restart": false, + "category": "scanner", + "group": "Scanning", + "description": "How often to scan all libraries (seconds)", + "is_default": true, + "reload_required": false +} +``` + +## Fields + +| Field | Type | Required | Notes | +| ----- | ---- | -------- | ----- | +| key | string | Yes | Known setting key | +| value | string | Yes | Value (validated against type/bounds) | + +## Errors + +| Status | Body | When | +| ------ | ---- | ---- | +| 400 | `{"error": "invalid request"}` / `{"error": "key is required"}` / `{"error": "unknown setting key: ..."}` / `{"error": "value must be ..."}` | malformed body or invalid value | +| 503 | `{"error": "settings registry not initialized"}` | registry not wired | \ No newline at end of file diff --git a/internal/docs/drift_test.go b/internal/docs/drift_test.go index 4878fbf9..54763e8e 100644 --- a/internal/docs/drift_test.go +++ b/internal/docs/drift_test.go @@ -221,6 +221,15 @@ func validateRequestExample(t *testing.T, doc *APIDoc, schema Schema) { return } + // AnyJSONObject request: arbitrary keys (config maps). Shape-check only. + if _, ok := schema.Request.(AnyJSONObject); ok { + var m map[string]json.RawMessage + if err := json.Unmarshal(doc.ExampleRequest, &m); err != nil { + t.Errorf("%s: example-request must be a JSON object", doc.RelPath) + } + return + } + // Typo guard: field names must exist on the bind struct. dec := json.NewDecoder(bytes.NewReader(doc.ExampleRequest)) dec.DisallowUnknownFields() @@ -257,6 +266,16 @@ func validateResponseExample(t *testing.T, doc *APIDoc, schema Schema) { return } + // AnyJSONObject: response is an object with arbitrary/dynamic keys + // (e.g. map[string]string config). Only shape-check, never key-check. + if _, ok := schema.Response.(AnyJSONObject); ok { + var m map[string]json.RawMessage + if err := json.Unmarshal(doc.ExampleResponse, &m); err != nil { + t.Errorf("%s: example-response must be a JSON object", doc.RelPath) + } + return + } + want, isSlice := responseTopLevelKeys(t, schema.Response) var got map[string]bool diff --git a/internal/docs/schema.go b/internal/docs/schema.go index 8dd1b974..6c6d84cd 100644 --- a/internal/docs/schema.go +++ b/internal/docs/schema.go @@ -34,6 +34,10 @@ type Schema struct { Response any } +// AnyJSONObject marks responses whose keys are dynamic (admin-defined config +// maps etc.): the gate then only verifies the example is a JSON object. +type AnyJSONObject struct{} + // Schemas is keyed by "METHOD path" in Echo syntax. Sample values inside // RawMessage pins use empty strings / zero values — only the KEY SET is // checked, never the values. @@ -254,4 +258,70 @@ var Schemas = map[string]Schema{ "GET /api/jobs/:jobId": { Response: services.JobResult{}, }, + + // --- WO3 tail batch: saved-filters, dashboard, system, series --- + "GET /api/saved-filters": { + Response: []handlers.SavedFilterResponse{}, + }, + "GET /api/saved-filters/:id": { + Response: handlers.SavedFilterResponse{}, + }, + "POST /api/saved-filters": { + Request: handlers.CreateSavedFilterRequest{}, + Response: handlers.SavedFilterResponse{}, + }, + "PUT /api/saved-filters/:id": { + Request: handlers.CreateSavedFilterRequest{}, // same struct reused for update + Response: handlers.SavedFilterResponse{}, + }, + "DELETE /api/saved-filters/:id": {}, + "GET /api/dashboard/sections": { + Response: json.RawMessage(`{"sections": []}`), + }, + "GET /api/dashboard/preferences": { + Response: json.RawMessage(`{ + "hidden_collections": [], "collection_order": [], + "items_per_section": 0 + }`), + }, + "PUT /api/dashboard/preferences": { + Request: json.RawMessage(`{ + "library_id": null, "hidden_collections": [], + "collection_order": [], "items_per_section": 0 + }`), + Response: database.UserDashboardPreferences{}, + }, + "POST /api/dashboard/restore-system-collection": { + Request: json.RawMessage(`{"collection_name": "", "reset_type": ""}`), + Response: json.RawMessage(`{"message": ""}`), + }, + "GET /api/system/config": { + // Admin-defined key/value map — arbitrary keys. + Response: AnyJSONObject{}, + }, + "PUT /api/system/config": { + Request: AnyJSONObject{}, // map[string]string of key -> new value + Response: json.RawMessage(`{"status": "", "message": ""}`), + }, + "GET /api/system/settings": { + Response: []database.SettingEntry{}, + }, + "PUT /api/system/settings": { + Request: handlers.UpdateSettingRequest{}, + Response: handlers.UpdateSettingResponse{}, + }, + "GET /api/series": { + Response: json.RawMessage(`{ + "series": [{ + "name": "", "library_id": "", "book_count": 0, + "total_in_series": 0, "cover_paths": [], "last_entry_at": "" + }], + "total": 0, "limit": 0, "offset": 0 + }`), + }, + "GET /api/series/books": { + Response: json.RawMessage(`{ + "name": "", "library_id": "", "books": [], "total": 0 + }`), + }, }