docs(saved-filters,dashboard,system,series): Beam WO3 batch 5 — 15 endpoints + dedupe
Beam drafted 15 frontmatter'd docs (saved-filters CRUD, dashboard
sections/preferences/restore, system config/settings, series list + books);
Claude verified shapes against handlers and pinned the registry:
- saved-filters: SavedFilterResponse pinned by type (RFC3339 timestamps here,
unlike most of the API); create/update share CreateSavedFilterRequest
(resource_type oneof media-items|collections|devices)
- dashboard: preferences GET pin {hidden_collections, collection_order,
items_per_section}; PUT returns full UserDashboardPreferences row; sections
wrapper {sections}
- system: GET+PUT /api/system/config are admin-defined key/value maps — new
AnyJSONObject sentinel (gate shape-checks without key comparison; request
side too). UpdateSystemConfiguration returns {status, message} — verified,
not guessed. GET /api/system/settings returns []SettingEntry.
- series: {series[{name, library_id, book_count, total_in_series,
cover_paths, last_entry_at}], total, limit, offset}; books by
?name=&library_id= with family-lens 403
Dedupe: deleted dashboard.md, system/config.md, system/settings.md — each
covered the same endpoints as Beam's per-endpoint files (content merged by
the gate-verified shapes; old prose recoverable from git).
Beam clarifications confirmed: scanner routes are WO5 (docs exist);
progress/books-match are WO5.
This commit is contained in:
1 parent
7471ca99e8
commit
fa00d6d3cb
20 files changed
+714
-416
No files matched your search
@@ -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 |
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
method: GET
|
||||
path: /api/dashboard/preferences
|
||||
auth: jwt
|
||||
area: dashboard
|
||||
---
|
||||
<!-- source: internal/handlers/dashboard.go:285 DashboardHandler.GetPreferences -->
|
||||
|
||||
# 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 |
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
method: GET
|
||||
path: /api/dashboard/sections
|
||||
auth: jwt
|
||||
area: dashboard
|
||||
---
|
||||
<!-- source: internal/handlers/dashboard.go:28 DashboardHandler.GetSections -->
|
||||
|
||||
# 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 |
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
method: POST
|
||||
path: /api/dashboard/restore-system-collection
|
||||
auth: jwt
|
||||
area: dashboard
|
||||
---
|
||||
<!-- source: internal/handlers/dashboard.go:132 DashboardHandler.RestoreSystemCollection -->
|
||||
|
||||
# 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 |
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
method: PUT
|
||||
path: /api/dashboard/preferences
|
||||
auth: jwt
|
||||
area: dashboard
|
||||
---
|
||||
<!-- source: internal/handlers/dashboard.go:83 DashboardHandler.UpdatePreferences -->
|
||||
|
||||
# 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 |
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
method: POST
|
||||
path: /api/saved-filters
|
||||
auth: jwt
|
||||
area: saved-filters
|
||||
---
|
||||
<!-- source: internal/handlers/filters.go:131 FiltersHandler.CreateSavedFilter -->
|
||||
|
||||
# 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 |
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
method: DELETE
|
||||
path: /api/saved-filters/:id
|
||||
auth: jwt
|
||||
area: saved-filters
|
||||
---
|
||||
<!-- source: internal/handlers/filters.go:178 FiltersHandler.DeleteSavedFilter -->
|
||||
|
||||
# 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.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
method: GET
|
||||
path: /api/saved-filters/:id
|
||||
auth: jwt
|
||||
area: saved-filters
|
||||
---
|
||||
<!-- source: internal/handlers/filters.go:91 FiltersHandler.GetSavedFilterByID -->
|
||||
|
||||
# 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 |
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
method: GET
|
||||
path: /api/saved-filters
|
||||
auth: jwt
|
||||
area: saved-filters
|
||||
---
|
||||
<!-- source: internal/handlers/filters.go:54 FiltersHandler.GetSavedFilters -->
|
||||
|
||||
# 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 |
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
method: PUT
|
||||
path: /api/saved-filters/:id
|
||||
auth: jwt
|
||||
area: saved-filters
|
||||
---
|
||||
<!-- source: internal/handlers/filters.go:200 FiltersHandler.UpdateSavedFilter -->
|
||||
|
||||
# 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 |
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
method: GET
|
||||
path: /api/series/books
|
||||
auth: jwt
|
||||
area: series
|
||||
---
|
||||
<!-- source: internal/handlers/series.go:112 SeriesHandler.GetSeriesBooks -->
|
||||
|
||||
# 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 |
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
method: GET
|
||||
path: /api/series
|
||||
auth: jwt
|
||||
area: series
|
||||
---
|
||||
<!-- source: internal/handlers/series.go:29 SeriesHandler.GetSeries -->
|
||||
|
||||
# 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 |
|
||||
@@ -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 <admin_token>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 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 <admin_token>" \
|
||||
-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/))
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
method: GET
|
||||
path: /api/system/config
|
||||
auth: jwt+admin
|
||||
area: system
|
||||
---
|
||||
<!-- source: internal/handlers/sidecar.go:374 SidecarHandler.GetSystemConfiguration -->
|
||||
|
||||
# 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 |
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
method: GET
|
||||
path: /api/system/settings
|
||||
auth: jwt+admin
|
||||
area: system
|
||||
---
|
||||
<!-- source: internal/handlers/system_settings.go:44 SystemSettingsHandler.GetSettings -->
|
||||
|
||||
# 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 |
|
||||
@@ -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 <admin_token>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 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 <admin_token>" \
|
||||
-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))
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
method: PUT
|
||||
path: /api/system/config
|
||||
auth: jwt+admin
|
||||
area: system
|
||||
---
|
||||
<!-- source: internal/handlers/sidecar.go:396 SidecarHandler.UpdateSystemConfiguration -->
|
||||
|
||||
# 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 |
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
method: PUT
|
||||
path: /api/system/settings
|
||||
auth: jwt+admin
|
||||
area: system
|
||||
---
|
||||
<!-- source: internal/handlers/system_settings.go:65 SystemSettingsHandler.UpdateSetting -->
|
||||
|
||||
# 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 |
|
||||
Reference in new issue
Block a user