From 6fc501b35a9f7b6ffb7da3b8a9d04564459d554d Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sat, 24 Oct 2026 18:25:12 -0400 Subject: [PATCH] =?UTF-8?q?docs(stats):=20Claude-side=20pinning=20?= =?UTF-8?q?=E2=80=94=20all=204=20stats=20endpoints=20+=20UNVERIFIED=20swee?= =?UTF-8?q?p=20to=20zero?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stats docs written handler/service-first (summary/this-week/book-stats new; library-usage frontmatter'd — its body was already accurate). Registry: 4 entries on services.UserSummaryStats/WeekSummaryStats/ LibraryUsageStats/BookStats. Family-lens note: library-usage 403s an explicit library_id outside the visible set (431fec0 rule). UNVERIFIED sweep (7 -> 0), each resolved against the handler: - device-usage takes NO date params; popular-books takes only ?limit (the 'standard analytics date-range params' claim was wrong) - link_book media_item_id: no validate tag but service-always-written - push_stats book-ref field set documented (uuid/sha256/title/authors) - filter_count: confirmed ParseMediaSearchParams machinery + 400 on bad filter + 403 visibility - facets values element shape pinned: {value, display?, count} - get_config: dynamic key set IS the contract (reworded, no marker) make test 9/9. --- .../api/analytics/get_device_usage.md | 4 +- .../api/analytics/get_popular_books.md | 8 +- docs/developer/api/devices/link_book.md | 2 +- docs/developer/api/koreader/push_stats.md | 4 +- .../developer/api/media-items/filter_count.md | 6 +- docs/developer/api/media-items/get_facets.md | 5 +- docs/developer/api/stats/get_book_stats.md | 60 ++++++++++ docs/developer/api/stats/get_library_usage.md | 10 +- docs/developer/api/stats/get_summary.md | 106 ++++++++++++++++++ docs/developer/api/stats/get_this_week.md | 48 ++++++++ docs/developer/api/system/get_config.md | 2 +- internal/docs/schema.go | 20 ++++ 12 files changed, 264 insertions(+), 11 deletions(-) create mode 100644 docs/developer/api/stats/get_book_stats.md create mode 100644 docs/developer/api/stats/get_summary.md create mode 100644 docs/developer/api/stats/get_this_week.md diff --git a/docs/developer/api/analytics/get_device_usage.md b/docs/developer/api/analytics/get_device_usage.md index da7f295a..ee613e90 100644 --- a/docs/developer/api/analytics/get_device_usage.md +++ b/docs/developer/api/analytics/get_device_usage.md @@ -30,8 +30,8 @@ Per-device sync activity for the signed-in user. ## Query parameters -UNVERIFIED: accepts the standard analytics date-range params; confirm exact -names before relying on them. +None. The handler aggregates the user's full device-usage history — there +is no date-range filtering on this endpoint. ## Errors diff --git a/docs/developer/api/analytics/get_popular_books.md b/docs/developer/api/analytics/get_popular_books.md index 0123c36a..59d8fbbf 100644 --- a/docs/developer/api/analytics/get_popular_books.md +++ b/docs/developer/api/analytics/get_popular_books.md @@ -29,8 +29,12 @@ Most-read books for the signed-in user. ## Query parameters -UNVERIFIED: accepts the standard analytics date-range params; confirm exact -names before relying on them. +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| limit | int | No | Result cap; default `10` | + +There is no date-range filtering on this endpoint — the ranking is +computed over all reading history. ## Errors diff --git a/docs/developer/api/devices/link_book.md b/docs/developer/api/devices/link_book.md index 94749e4d..52a6a3a6 100644 --- a/docs/developer/api/devices/link_book.md +++ b/docs/developer/api/devices/link_book.md @@ -48,7 +48,7 @@ confidence score. Used by the book-matching flow. | device_file.file_path | string | no | path on the device | | device_file.sha256 | string | no | file content hash | | device_file.title | string | no | device-side title | -| media_item_id | string (uuid) | yes | `services.LinkBookRequest` declares it as uuid; the gate only checks the Bind struct — UNVERIFIED: confirm required tag on LinkBookRequest fields | +| media_item_id | string (uuid) | yes | no validate tag, but the service always writes it (upsert keyed on media item) — omitting it orphans the alias | | confidence_score | number | no | match confidence (0-1) | ## Errors diff --git a/docs/developer/api/koreader/push_stats.md b/docs/developer/api/koreader/push_stats.md index 24716c38..b2a7b522 100644 --- a/docs/developer/api/koreader/push_stats.md +++ b/docs/developer/api/koreader/push_stats.md @@ -31,4 +31,6 @@ stored watermark becomes a reading_history session. } ``` -UNVERIFIED: the request body also carries the koreader book reference fields (md5, title, etc.) used to resolve the book; field set not copied here. \ No newline at end of file +The request body also embeds the koreader book reference used to resolve +the book — fields (all optional, resolved via the same priority chain as +progress sync): `uuid`, `sha256`, `title`, `authors` (string array). \ No newline at end of file diff --git a/docs/developer/api/media-items/filter_count.md b/docs/developer/api/media-items/filter_count.md index 873cc9b5..183509ec 100644 --- a/docs/developer/api/media-items/filter_count.md +++ b/docs/developer/api/media-items/filter_count.md @@ -21,8 +21,10 @@ requester's visible libraries. Used by the filter UI's live count. ## Query parameters -UNVERIFIED: filter parameters mirror the media-items list endpoint (same -filter machinery); pass them through unchanged. +Confirmed: filter parameters use the exact same machinery as the +media-items list endpoint (`ParseMediaSearchParams`); an invalid filter +expression returns **400** `invalid filter: ...` (not a silent pass). +Library visibility is enforced the same way (403 `library not visible`). ## Errors diff --git a/docs/developer/api/media-items/get_facets.md b/docs/developer/api/media-items/get_facets.md index 9837e07a..2f0ba45f 100644 --- a/docs/developer/api/media-items/get_facets.md +++ b/docs/developer/api/media-items/get_facets.md @@ -28,7 +28,10 @@ Two modes, both returning small JSON objects: } ``` -UNVERIFIED: exact element shape of `values` (service-returned rows). +`values` elements are `{value, display?, count}` rows (`FacetValue`): +`value` is the raw value that rides the filter param, `display` is the +prettified form when it differs (language codes → names; absent means the +client shows `value`), `count` is the number of matching items. ## Query parameters diff --git a/docs/developer/api/stats/get_book_stats.md b/docs/developer/api/stats/get_book_stats.md new file mode 100644 index 00000000..e62284a0 --- /dev/null +++ b/docs/developer/api/stats/get_book_stats.md @@ -0,0 +1,60 @@ +--- +method: GET +path: /api/stats/books/:id +auth: jwt +area: stats +--- + + +# Get Book Stats + +Per-book reading card on the book detail page: lifetime totals plus recent +sessions (newest first, capped). Scoped to the requesting user's own +reading history. + +## Example Request + +```http +GET /api/stats/books/550e8400-e29b-41d4-a716-446655440000 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` + +## Example response + +```json example-response +{ + "media_item_id": "550e8400-e29b-41d4-a716-446655440000", + "total_minutes": 610, + "total_seconds": 36612, + "total_pages": 0, + "session_count": 41, + "max_progress": 0.62, + "finished": false, + "first_session": "2026-09-30T18:02:11Z", + "last_session": "2026-10-23T21:44:05Z", + "recent_sessions": [ + { + "started_at": "2026-10-23T21:44:05Z", + "ended_at": "2026-10-23T22:19:40Z", + "minutes": 35, + "pages": 0, + "device_id": "660e8400-e29b-41d4-a716-446655440001" + } + ] +} +``` + +## Response fields + +| Field | Type | Notes | +| ------------- | ------- | ---------------------------------------------------------------- | +| total_seconds | int | exact wall-clock (audio "Time listened"; minutes lose remainder) | +| max_progress | float | highest progress fraction ever reached (0-1) | +| recent_sessions | array | newest first, server-capped | + +## Errors + +| Status | Body | When | +| ------ | ------------------------ | ----------------------- | +| 400 | `invalid media item id` | malformed id | +| 401 | `invalid user` | no user in JWT context | diff --git a/docs/developer/api/stats/get_library_usage.md b/docs/developer/api/stats/get_library_usage.md index f7adb00b..b390a3d6 100644 --- a/docs/developer/api/stats/get_library_usage.md +++ b/docs/developer/api/stats/get_library_usage.md @@ -1,3 +1,11 @@ +--- +method: GET +path: /api/stats/library-usage +auth: jwt +area: stats +--- + + # Get Library Usage Statistics The `/stats` page's **Library tab** payload: media_items **inventory** @@ -29,7 +37,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ## Response (200 OK) -```json +```json example-response { "library_id": "8c8d0cee-1cca-4def-9a37-edad3a4d438b", "items": 743, diff --git a/docs/developer/api/stats/get_summary.md b/docs/developer/api/stats/get_summary.md new file mode 100644 index 00000000..7859f664 --- /dev/null +++ b/docs/developer/api/stats/get_summary.md @@ -0,0 +1,106 @@ +--- +method: GET +path: /api/stats/summary +auth: jwt +area: stats +--- + + +# Get Reading Summary + +The `/stats` page's complete payload: totals, zero-filled daily arrays, +streaks, most-active day, top books, and per-library/per-device breakdowns +for a time range. All day boundaries and buckets are computed in the +**requesting user's timezone**. + +## Query Parameters + +| Parameter | Type | Required | Description | +| --------- | ------ | -------- | -------------------------------------------------- | +| range | string | No | `7` (default), `30`, `90`, or `year` (days window) | + +Invalid `range` values return **400**. + +## Example Request + +```http +GET /api/stats/summary?range=30 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` + +## Example response + +```json example-response +{ + "range": "30", + "days": 30, + "start_date": "2026-09-24", + "end_date": "2026-10-24", + "total_minutes": 1250, + "total_audio_minutes": 1100, + "total_pages": 412, + "session_count": 96, + "books_with_activity": 7, + "books_finished": 2, + "completion_rate": 0.29, + "average_session_minutes": 13.0, + "daily": [ + { + "date": "2026-09-24", + "minutes": 42, + "pages": 15, + "sessions": 3, + "audio_minutes": 42 + } + ], + "streak": { "current": 5, "longest": 12 }, + "most_active_weekday": "Saturday", + "most_active_day": "2026-10-17", + "most_active_day_minutes": 87, + "top_books": [ + { + "media_item_id": "550e8400-e29b-41d4-a716-446655440000", + "title": "Gardens of the Moon", + "author": "Steven Erikson", + "minutes": 610, + "pages": 0 + } + ], + "library_breakdown": [ + { + "library_id": "8c8d0cee-1cca-4def-9a37-edad3a4d438b", + "name": "Audiobooks", + "minutes": 1100, + "pages": 0, + "sessions": 84, + "books": 5 + } + ], + "device_breakdown": [ + { "device_id": "", "name": "Web", "type": "web", "minutes": 150 }, + { + "device_id": "660e8400-e29b-41d4-a716-446655440001", + "name": "Pixel 9", + "type": "android", + "minutes": 1100 + } + ] +} +``` + +## Response fields + +| Field | Type | Notes | +| ---------------- | ------- | --------------------------------------------------------------------- | +| total_audio_minutes | int | listening minutes inside `total_minutes` (mode-aware wording surface) | +| daily | array | zero-filled across the whole range — every day present | +| streak | object | `current` / `longest` day streaks | +| most_active_weekday | string | `""` when the range has no sessions | +| device_breakdown | array | `device_id` is `""` for the web bucket (pushes with no device credential) | + +## Errors + +| Status | Body | When | +| ------ | -------------------------- | ----------------------- | +| 400 | `invalid range ...` | unknown range key | +| 401 | `invalid user` | no user in JWT context | diff --git a/docs/developer/api/stats/get_this_week.md b/docs/developer/api/stats/get_this_week.md new file mode 100644 index 00000000..eee6003d --- /dev/null +++ b/docs/developer/api/stats/get_this_week.md @@ -0,0 +1,48 @@ +--- +method: GET +path: /api/stats/this-week +auth: jwt +area: stats +--- + + +# Get This Week's Stats + +Compact week-vs-previous-week comparison powering the dashboard widget. +Week boundaries are the requesting user's calendar week (timezone-aware). + +## Query Parameters + +| Parameter | Type | Required | Description | +| ---------- | ------------- | -------- | --------------------------------------- | +| library_id | string (UUID) | No | Scope totals to one library | + +## Example Request + +```http +GET /api/stats/this-week +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` + +## Example response + +```json example-response +{ + "week_minutes": 315, + "week_pages": 122, + "week_sessions": 21, + "week_books": 3, + "prev_week_minutes": 402, + "prev_week_pages": 160, + "prev_week_sessions": 25, + "active_today": true, + "current_streak": 5 +} +``` + +## Errors + +| Status | Body | When | +| ------ | -------------------- | ---------------------- | +| 400 | `invalid library id` | malformed library_id | +| 401 | `invalid user` | no user in JWT context | diff --git a/docs/developer/api/system/get_config.md b/docs/developer/api/system/get_config.md index 56f4c28b..820752aa 100644 --- a/docs/developer/api/system/get_config.md +++ b/docs/developer/api/system/get_config.md @@ -21,7 +21,7 @@ Returns all system-wide configuration key/value pairs (backed by the } ``` -UNVERIFIED: exact config key set is arbitrary (admin-defined); example shows common keys only. +The key set is admin-defined and intentionally open — the endpoint emits\nwhatever config keys exist in `system_settings`. Treat the shape as a\ndynamic string-keyed map; do not code against a fixed key list. ## Errors diff --git a/internal/docs/schema.go b/internal/docs/schema.go index d293132c..8dddf544 100644 --- a/internal/docs/schema.go +++ b/internal/docs/schema.go @@ -957,4 +957,24 @@ var Schemas = map[string]Schema{ // renderer — deliberately NOT documented (rule 3). Response: json.RawMessage(`{"conflicts": [], "total": 0}`), }, + + // --- Claude: stats (service-backed reading statistics) --- + "GET /api/stats/summary": { + // ?range=7|30|90|year; zero-filled daily arrays; user-tz buckets. + Response: services.UserSummaryStats{}, + }, + "GET /api/stats/this-week": { + // ?library_id= optional (visible-set enforced like library-usage? no — + // this route passes it straight through). + Response: services.WeekSummaryStats{}, + }, + "GET /api/stats/library-usage": { + // ?library_id= optional; explicit id OUTSIDE the visible set → 403. + // Inventory aggregates (media_items), no reading_history. + Response: services.LibraryUsageStats{}, + }, + "GET /api/stats/books/:id": { + // Lifetime totals + recent sessions for one media item. + Response: services.BookStats{}, + }, }