Files
bookhoard/docs/developer/api/stats/get_summary.md
T
John O'Keefe 6fc501b35a docs(stats): Claude-side pinning — all 4 stats endpoints + UNVERIFIED sweep to zero
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.
2026-10-24 18:25:12 -04:00

3.2 KiB

method, path, auth, area
method path auth area
GET /api/stats/summary jwt 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

GET /api/stats/summary?range=30
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

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