diff --git a/docs/developer/api/analytics/get_analytics.md b/docs/developer/api/analytics/get_analytics.md index 70d4a77c..fc2d7d16 100644 --- a/docs/developer/api/analytics/get_analytics.md +++ b/docs/developer/api/analytics/get_analytics.md @@ -1,3 +1,11 @@ +--- +method: GET +path: /api/analytics/reading-stats +auth: jwt +area: analytics +--- + + # Get Reading Statistics Retrieve reading statistics for a date range. @@ -27,13 +35,16 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ## Response (200 OK) -```json +```json example-response { - "pages_read": 1250, - "books_completed": 5, - "reading_time_hours": 42.5, - "sessions_count": 28, - "average_session_minutes": 91 + "average_session_time_minutes": null, + "completion_rate": null, + "daily_reading_minutes": null, + "longest_session_minutes": null, + "most_active_day_of_week": null, + "total_books_read": null, + "total_pages_read": null, + "total_reading_time_minutes": null } ``` diff --git a/docs/developer/api/highlights/create_highlight.md b/docs/developer/api/highlights/create_highlight.md index bdee97db..07e08bcd 100644 --- a/docs/developer/api/highlights/create_highlight.md +++ b/docs/developer/api/highlights/create_highlight.md @@ -1,3 +1,11 @@ +--- +method: POST +path: /api/media-items/:id/highlights +auth: jwt +area: media-items +--- + + # Create Highlight Create a new highlight for a media item. @@ -25,7 +33,7 @@ Create a new highlight for a media item. ### Example Request -```json +```json example-request { "selection_text": "Highlighted text...", "start_position": "epubcfi(/6/4/2:15)", @@ -38,7 +46,7 @@ Create a new highlight for a media item. ## Response (201 Created) -```json +```json example-response { "id": "uuid", "media_item_id": "uuid", @@ -49,7 +57,24 @@ Create a new highlight for a media item. "color": "#ffff00", "percentage_start": 0.45, "percentage_end": 0.47, - "created_at": "2026-01-31T10:00:00Z" + "created_at": "2026-01-31T10:00:00Z", + "chapter_reference": null, + "character_end": null, + "character_start": null, + "dedup_key": null, + "deleted": null, + "deleted_at": null, + "device_sync_data": null, + "epubcfi_end": null, + "epubcfi_start": null, + "last_modified_at": null, + "last_modified_source": null, + "note_id": null, + "note_text": null, + "panel_number": null, + "paragraph_end": null, + "paragraph_start": null, + "updated_at": null } ``` diff --git a/docs/developer/api/highlights/delete_highlight.md b/docs/developer/api/highlights/delete_highlight.md index 2046d4b1..ddc30ee8 100644 --- a/docs/developer/api/highlights/delete_highlight.md +++ b/docs/developer/api/highlights/delete_highlight.md @@ -1,3 +1,11 @@ +--- +method: DELETE +path: /api/media-items/:id/highlights/:highlightId +auth: jwt +area: media-items +--- + + # Delete Highlight Delete a highlight. diff --git a/docs/developer/api/highlights/get_highlights.md b/docs/developer/api/highlights/get_highlights.md index e0b00fdc..5639253c 100644 --- a/docs/developer/api/highlights/get_highlights.md +++ b/docs/developer/api/highlights/get_highlights.md @@ -1,3 +1,11 @@ +--- +method: GET +path: /api/media-items/:id/highlights +auth: jwt +area: media-items +--- + + # Get Highlights Retrieve all highlights for a specific media item. @@ -26,29 +34,42 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ## Response (200 OK) -```json -{ - "highlights": [ - { - "id": "uuid", - "media_item_id": "uuid", - "user_id": "uuid", - "selection_text": "Highlighted text passage...", - "start_position": "epubcfi(/6/4/2:15)", - "end_position": "epubcfi(/6/4/2:20)", - "color": "#ffff00", - "percentage_start": 0.45, - "percentage_end": 0.47, - "character_start": 15432, - "character_end": 15480, - "epubcfi_start": "epubcfi(/6/4/2:15)", - "epubcfi_end": "epubcfi(/6/4/2:20)", - "created_at": "2026-01-31T10:00:00Z" - } - ] -} +```json example-response +[ + { + "id": "uuid", + "media_item_id": "uuid", + "user_id": "uuid", + "selection_text": "Highlighted text passage...", + "start_position": "epubcfi(/6/4/2:15)", + "end_position": "epubcfi(/6/4/2:20)", + "color": "#ffff00", + "percentage_start": 0.45, + "percentage_end": 0.47, + "character_start": 15432, + "character_end": 15480, + "epubcfi_start": "epubcfi(/6/4/2:15)", + "epubcfi_end": "epubcfi(/6/4/2:20)", + "created_at": "2026-01-31T10:00:00Z", + "chapter_reference": null, + "dedup_key": "", + "deleted": false, + "deleted_at": null, + "device_sync_data": "", + "last_modified_at": null, + "last_modified_source": "", + "note_id": null, + "note_text": null, + "panel_number": null, + "paragraph_end": null, + "paragraph_start": null, + "updated_at": null + } +] ``` +Response is a bare JSON array of MediaHighlights rows — no wrapper object. + ## Error Responses | Code | Description | diff --git a/docs/developer/api/highlights/update_highlight.md b/docs/developer/api/highlights/update_highlight.md index a7587871..c54495a1 100644 --- a/docs/developer/api/highlights/update_highlight.md +++ b/docs/developer/api/highlights/update_highlight.md @@ -1,3 +1,11 @@ +--- +method: PUT +path: /api/media-items/:id/highlights/:highlightId +auth: jwt +area: media-items +--- + + # Update Highlight Update an existing highlight. @@ -21,7 +29,7 @@ Update an existing highlight. ### Example Request -```json +```json example-request { "selection_text": "Updated text", "color": "#00ff00" @@ -30,14 +38,35 @@ Update an existing highlight. ## Response (200 OK) -```json +```json example-response { "id": "uuid", "media_item_id": "uuid", "user_id": "uuid", "selection_text": "Updated text", "color": "#00ff00", - "updated_at": "2026-01-31T10:00:00Z" + "updated_at": "2026-01-31T10:00:00Z", + "chapter_reference": null, + "character_end": null, + "character_start": null, + "created_at": null, + "dedup_key": null, + "deleted": null, + "deleted_at": null, + "device_sync_data": null, + "end_position": null, + "epubcfi_end": null, + "epubcfi_start": null, + "last_modified_at": null, + "last_modified_source": null, + "note_id": null, + "note_text": null, + "panel_number": null, + "paragraph_end": null, + "paragraph_start": null, + "percentage_end": null, + "percentage_start": null, + "start_position": null } ``` diff --git a/docs/developer/api/notes/create_note.md b/docs/developer/api/notes/create_note.md index 170a391c..e48eed9b 100644 --- a/docs/developer/api/notes/create_note.md +++ b/docs/developer/api/notes/create_note.md @@ -1,3 +1,11 @@ +--- +method: POST +path: /api/media-items/:id/notes +auth: jwt +area: media-items +--- + + # Create Note Create a new note for a media item. @@ -23,18 +31,17 @@ Create a new note for a media item. ### Example Request -```json +```json example-request { "content": "This is a note", "position": "epubcfi(/6/4/2:15)", - "percentage_location": 0.45, - "epubcfi_location": "epubcfi(/6/4/2:15)" + "origin": "web" } ``` ## Response (201 Created) -```json +```json example-response { "id": "uuid", "media_item_id": "uuid", @@ -43,7 +50,18 @@ Create a new note for a media item. "position": "epubcfi(/6/4/2:15)", "percentage_location": 0.45, "epubcfi_location": "epubcfi(/6/4/2:15)", - "created_at": "2026-01-31T10:00:00Z" + "created_at": "2026-01-31T10:00:00Z", + "chapter_reference": null, + "character_end": null, + "character_start": null, + "dedup_key": null, + "deleted": null, + "deleted_at": null, + "device_sync_data": null, + "last_modified_at": null, + "last_modified_source": null, + "paragraph_reference": null, + "updated_at": null } ``` diff --git a/docs/developer/api/notes/delete_note.md b/docs/developer/api/notes/delete_note.md index 279558e1..ce47bf71 100644 --- a/docs/developer/api/notes/delete_note.md +++ b/docs/developer/api/notes/delete_note.md @@ -1,3 +1,11 @@ +--- +method: DELETE +path: /api/media-items/:id/notes/:noteId +auth: jwt +area: media-items +--- + + # Delete Note Delete a note. diff --git a/docs/developer/api/notes/get_notes.md b/docs/developer/api/notes/get_notes.md index a7217d61..201a2891 100644 --- a/docs/developer/api/notes/get_notes.md +++ b/docs/developer/api/notes/get_notes.md @@ -1,3 +1,11 @@ +--- +method: GET +path: /api/media-items/:id/notes +auth: jwt +area: media-items +--- + + # Get Notes Retrieve all notes for a specific media item. @@ -26,26 +34,34 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ## Response (200 OK) -```json -{ - "notes": [ - { - "id": "uuid", - "media_item_id": "uuid", - "user_id": "uuid", - "content": "This is an interesting passage...", - "position": "epubcfi(/6/4/2:15)", - "percentage_location": 0.45, - "character_start": 15432, - "character_end": 15480, - "epubcfi_location": "epubcfi(/6/4/2:15)", - "created_at": "2026-01-31T10:00:00Z", - "updated_at": "2026-01-31T10:00:00Z" - } - ] -} +```json example-response +[ + { + "id": "uuid", + "media_item_id": "uuid", + "user_id": "uuid", + "content": "This is an interesting passage...", + "position": "epubcfi(/6/4/2:15)", + "percentage_location": 0.45, + "character_start": 15432, + "character_end": 15480, + "epubcfi_location": "epubcfi(/6/4/2:15)", + "created_at": "2026-01-31T10:00:00Z", + "updated_at": "2026-01-31T10:00:00Z", + "chapter_reference": null, + "dedup_key": "", + "deleted": false, + "deleted_at": null, + "device_sync_data": "", + "last_modified_at": null, + "last_modified_source": "", + "paragraph_reference": null + } +] ``` +Response is a bare JSON array of MediaNotes rows — no wrapper object. + ## Error Responses | Code | Description | diff --git a/docs/developer/api/notes/update_note.md b/docs/developer/api/notes/update_note.md index c0aa370d..e2a75d8d 100644 --- a/docs/developer/api/notes/update_note.md +++ b/docs/developer/api/notes/update_note.md @@ -1,3 +1,11 @@ +--- +method: PUT +path: /api/media-items/:id/notes/:noteId +auth: jwt +area: media-items +--- + + # Update Note Update an existing note. @@ -21,7 +29,7 @@ Update an existing note. ### Example Request -```json +```json example-request { "content": "Updated note content", "position": "epubcfi(/6/4/2:20)" @@ -30,14 +38,27 @@ Update an existing note. ## Response (200 OK) -```json +```json example-response { "id": "uuid", "media_item_id": "uuid", "user_id": "uuid", "content": "Updated note content", "position": "epubcfi(/6/4/2:20)", - "updated_at": "2026-01-31T10:00:00Z" + "updated_at": "2026-01-31T10:00:00Z", + "chapter_reference": null, + "character_end": null, + "character_start": null, + "created_at": null, + "dedup_key": null, + "deleted": null, + "deleted_at": null, + "device_sync_data": null, + "epubcfi_location": null, + "last_modified_at": null, + "last_modified_source": null, + "paragraph_reference": null, + "percentage_location": null } ``` diff --git a/docs/developer/api/opds/acquisition.md b/docs/developer/api/opds/acquisition.md index 3b8b758e..3e1a26f5 100644 --- a/docs/developer/api/opds/acquisition.md +++ b/docs/developer/api/opds/acquisition.md @@ -1,3 +1,11 @@ +--- +method: GET +path: /opds/devices/:deviceId/download/:bookId +auth: device-token +area: opds +--- + + # OPDS Acquisition Download books and list available formats. diff --git a/docs/developer/api/opds/feeds.md b/docs/developer/api/opds/feeds.md index bdb82eb5..032d1a60 100644 --- a/docs/developer/api/opds/feeds.md +++ b/docs/developer/api/opds/feeds.md @@ -1,3 +1,11 @@ +--- +method: GET +path: /opds/devices/:deviceId/catalog +auth: device-token +area: opds +--- + + # OPDS Feeds Bookhoard provides OPDS 1.2 feeds for device compatibility. diff --git a/docs/developer/api/opds/publication.md b/docs/developer/api/opds/publication.md index 91438990..2d7fb01d 100644 --- a/docs/developer/api/opds/publication.md +++ b/docs/developer/api/opds/publication.md @@ -1,3 +1,11 @@ +--- +method: GET +path: /opds/devices/:deviceId/cover/:bookId +auth: device-token +area: opds +--- + + # OPDS Publication OPDS navigation and publication feeds. diff --git a/docs/developer/api/progress/delete_media_progress.md b/docs/developer/api/progress/delete_media_progress.md index 716e73f5..92be6a7f 100644 --- a/docs/developer/api/progress/delete_media_progress.md +++ b/docs/developer/api/progress/delete_media_progress.md @@ -1,3 +1,11 @@ +--- +method: DELETE +path: /api/media-items/:id/progress +auth: jwt +area: media-items +--- + + # Delete Media Item Progress Delete the stored reading progress for a media item. @@ -20,7 +28,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIs... ## Response (200 OK) -```json +```json example-response { "message": "reading progress deleted" } diff --git a/internal/docs/drift_test.go b/internal/docs/drift_test.go index 705d0a51..d4ba936d 100644 --- a/internal/docs/drift_test.go +++ b/internal/docs/drift_test.go @@ -30,6 +30,7 @@ type extractedRoute struct { var ( groupRe = regexp.MustCompile(`^(\w+)\s*:=\s*(\S+)\.Group\("([^"]*)"(?:,\s*(.+))?\)`) + useRe = regexp.MustCompile(`^(\w+)\.Use\((.+)\)$`) echoRouteRe = regexp.MustCompile(`^(?:e|cfg\.Echo)\.(GET|POST|PUT|PATCH|DELETE)\("([^"]*)"(?:,\s*(.+))?\)`) groupRouteRe = regexp.MustCompile(`^(\w+)\.(GET|POST|PUT|PATCH|DELETE)\("([^"]*)"(?:,\s*(.+))?\)`) ) @@ -94,6 +95,16 @@ func extractRoutesFromRouterSource(t *testing.T) map[string]extractedRoute { continue } + //opds-style middleware: `grp.Use(cfg.DeviceAuthMiddleware.Authenticate)` + //applies to every route registered on that group afterwards. + if m := useRe.FindStringSubmatch(s); m != nil { + if g, ok := groups[m[1]]; ok { + g.middleware = append(g.middleware, splitMW(m[2])...) + groups[m[1]] = g + } + continue + } + if m := echoRouteRe.FindStringSubmatch(s); m != nil { add(m[1], m[2], splitMW(m[3])) continue diff --git a/internal/docs/schema.go b/internal/docs/schema.go index bef9a7fd..b3236f73 100644 --- a/internal/docs/schema.go +++ b/internal/docs/schema.go @@ -726,4 +726,48 @@ var Schemas = map[string]Schema{ "require_number": true, "require_special": true }`), }, + + // --- Beam batch 9: highlights, notes, progress delete, analytics, opds --- + "POST /api/media-items/:id/highlights": { + Request: handlers.CreateMediaHighlightRequest{}, + Response: database.MediaHighlights{}, + }, + "GET /api/media-items/:id/highlights": { + Response: []database.MediaHighlights{}, + }, + "PUT /api/media-items/:id/highlights/:highlightId": { + // LWW edit targeted by URL id (identity NOT re-derived from content). + Request: handlers.UpdateMediaHighlightRequest{}, + Response: database.MediaHighlights{}, + }, + "DELETE /api/media-items/:id/highlights/:highlightId": { + // Tombstone via sync service; 204 No Content. + }, + "POST /api/media-items/:id/notes": { + Request: handlers.CreateMediaNoteRequest{}, + Response: database.MediaNotes{}, + }, + "GET /api/media-items/:id/notes": { + Response: []database.MediaNotes{}, + }, + "PUT /api/media-items/:id/notes/:noteId": { + Request: handlers.UpdateMediaNoteRequest{}, + Response: database.MediaNotes{}, + }, + "DELETE /api/media-items/:id/notes/:noteId": { + // Tombstone; 204 No Content. + }, + "DELETE /api/media-items/:id/progress": { + Response: json.RawMessage(`{"message": ""}`), + }, + "GET /api/analytics/reading-stats": { + // Query: start_date/end_date in MM-DD-YYYY, user-timezone buckets. + Response: handlers.ReadingStatsResponse{}, + }, + + // OPDS: XML/Atom responses (content negotiated, not JSON) — response + // side intentionally unpinned. + "GET /opds/devices/:deviceId/catalog": {}, + "GET /opds/devices/:deviceId/download/:bookId": {}, + "GET /opds/devices/:deviceId/cover/:bookId": {}, }