The combined profile handler (self-edit PUT /api/auth/profile and admin PUT /api/auth/profile/:id) binds one struct for both paths — and it's the admin-named one. Its twin UpdateProfileRequest had zero references left. Delete it and rename AdminUpdateUserRequest to ProfileUpdateRequest so the name matches who actually binds it; the comments describing field semantics now stand on their own instead of pointing at the deleted twin. Wire format (json/form tags) unchanged; docs schema registration follows the rename.
983 lines
33 KiB
Go
983 lines
33 KiB
Go
package docs
|
|
|
|
import (
|
|
"encoding/json"
|
|
|
|
"bookhoard/internal/database"
|
|
"bookhoard/internal/handlers"
|
|
"bookhoard/internal/services"
|
|
"bookhoard/internal/sync"
|
|
)
|
|
|
|
// Schema pins the machine-checkable contract for one route. The drift gate
|
|
// (drift_test.go) validates every frontmatter'd doc against the entry for its
|
|
// "METHOD path" key.
|
|
//
|
|
// MAINTENANCE CONTRACT: every doc with frontmatter MUST have a registry
|
|
// entry, and every new/changed route that ships a frontmatter'd doc updates
|
|
// its entry in the same commit. The gate fails loudly on a missing entry —
|
|
// that is the discipline, not an inconvenience.
|
|
//
|
|
// Request: the struct the handler Binds into (nil for bodyless routes).
|
|
// - example-request is decoded with unknown fields DISALLOWED (typo guard)
|
|
// - fields whose validate tag contains "required" must appear in the
|
|
// example (staleness guard)
|
|
//
|
|
// Response: what the handler passes to c.JSON.
|
|
// - struct / slice of struct: marshaled and its TOP-LEVEL key set compared
|
|
// against example-response in both directions
|
|
// - json.RawMessage: hand-built map responses pin their key set as a
|
|
// sample; promote to a struct when the endpoint is next touched
|
|
// - zero value (Schema{}): no JSON body expected (e.g. 204) — the doc must
|
|
// not carry an example-response block
|
|
type Schema struct {
|
|
Request any
|
|
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.
|
|
var Schemas = map[string]Schema{
|
|
// --- collections (pilot area) ---
|
|
"POST /api/collections": {
|
|
Request: handlers.CreateCollectionRequest{},
|
|
Response: json.RawMessage(`{
|
|
"id": "", "user_id": "", "name": "", "description": "",
|
|
"color": "", "icon": "", "collection_type": "",
|
|
"auto_assign_rules": null, "view_settings": {},
|
|
"book_count": 0, "created_at": ""
|
|
}`),
|
|
},
|
|
"GET /api/collections": {
|
|
Response: struct {
|
|
Collections []handlers.CollectionSummary `json:"collections"`
|
|
Total int `json:"total"`
|
|
}{},
|
|
},
|
|
"GET /api/collections/:id": {
|
|
Response: json.RawMessage(`{
|
|
"id": "", "user_id": "", "name": "", "description": "",
|
|
"color": "", "icon": "", "auto_assign_rules": null,
|
|
"view_settings": {}, "books": [], "book_count": 0,
|
|
"is_smart": false, "up_next": [], "created_at": ""
|
|
}`),
|
|
},
|
|
"PUT /api/collections/:id": {
|
|
Request: handlers.UpdateCollectionRequest{},
|
|
Response: json.RawMessage(`{
|
|
"id": "", "user_id": "", "name": "", "description": "",
|
|
"color": "", "icon": "", "auto_assign_rules": null,
|
|
"view_settings": {}, "created_at": ""
|
|
}`),
|
|
},
|
|
"DELETE /api/collections/:id": {},
|
|
// --- devices (batch 1) ---
|
|
"GET /api/devices/:id": {
|
|
Response: handlers.DeviceInfo{},
|
|
},
|
|
"PUT /api/devices/:id/regenerate-token": {
|
|
Response: json.RawMessage(`{
|
|
"message": "", "auth_token": "",
|
|
"device": {
|
|
"id": "", "device_name": "", "device_type": "",
|
|
"last_sync": null, "last_seen": null,
|
|
"sync_enabled": false, "auto_sync": false,
|
|
"sync_frequency_minutes": 0, "created_at": "",
|
|
"device_metadata": null, "auth_token": ""
|
|
},
|
|
"sync_urls": {}
|
|
}`),
|
|
},
|
|
"GET /api/devices/:id/collections": {
|
|
Response: struct {
|
|
DeviceID string `json:"device_id"`
|
|
Mappings []handlers.DeviceMappingSummary `json:"mappings"`
|
|
Total int `json:"total"`
|
|
}{},
|
|
},
|
|
"PUT /api/devices/:id/collections/:collectionId": {
|
|
Request: handlers.UpdateDeviceMappingRequest{},
|
|
Response: json.RawMessage(`{
|
|
"id": "", "collection_id": "", "device_id": "",
|
|
"device_shelf_name": "", "sync_direction": "", "created_at": ""
|
|
}`),
|
|
},
|
|
"GET /api/devices/:id/sidecar/download": {
|
|
Response: json.RawMessage(`{
|
|
"version": "", "bookhoard": {}, "books": {},
|
|
"collections": [], "opds_enabled": false,
|
|
"sidecar_enabled": false, "last_updated": ""
|
|
}`),
|
|
},
|
|
"POST /api/devices/:deviceId/sync/link-book": {
|
|
Request: services.LinkBookRequest{},
|
|
Response: json.RawMessage(`{
|
|
"status": "",
|
|
"device_file_alias": {
|
|
"id": "", "media_item_id": "", "device_id": "",
|
|
"file_path": "", "file_sha256": "",
|
|
"confidence_score": 0.0
|
|
}
|
|
}`),
|
|
},
|
|
|
|
// --- libraries (batch 2) ---
|
|
"GET /api/libraries/types": {
|
|
Response: []database.LibraryTypes{},
|
|
},
|
|
"GET /api/libraries": {
|
|
Response: struct {
|
|
Data []database.ListLibrariesRow `json:"data"`
|
|
}{},
|
|
},
|
|
"GET /api/libraries/:id/folders": {
|
|
Response: []database.LibraryFolders{},
|
|
},
|
|
"GET /api/libraries/:id/issues/list": {
|
|
Response: []handlers.ProcessingIssueResponse{},
|
|
},
|
|
"GET /api/libraries/:id/issues/stats": {
|
|
Response: handlers.ProcessingIssueStats{},
|
|
},
|
|
"POST /api/libraries/:id/issues/:issueId/:mediaItemId/resolve": {
|
|
Response: json.RawMessage(`{"success": false, "message": ""}`),
|
|
},
|
|
"DELETE /api/libraries/:id/issues/:issueId": {
|
|
Response: json.RawMessage(`{"success": false, "message": ""}`),
|
|
},
|
|
"POST /api/libraries/:id/scan": {
|
|
Request: nil, // library_id injected from the path; no body used
|
|
Response: json.RawMessage(`{"message": "", "job_id": "", "status": ""}`),
|
|
},
|
|
"GET /api/libraries/scan-settings": {
|
|
Response: handlers.ScanSettingsResponse{},
|
|
},
|
|
"PUT /api/libraries/scan-settings": {
|
|
Request: handlers.UpdateScanSettingsRequest{},
|
|
// RawMessage pin (not the struct): the update handler ALWAYS sets
|
|
// Message, but the struct's omitempty would drop it from a
|
|
// zero-value marshal and the gate would reject correct docs.
|
|
Response: json.RawMessage(`{
|
|
"scan_poll_interval_seconds": 0,
|
|
"auto_scan_enabled": false,
|
|
"message": ""
|
|
}`),
|
|
},
|
|
|
|
// --- media-items batch 3a: reader-adjacent CRUD ---
|
|
"GET /api/media-items/:id/bookmarks": {
|
|
Response: []database.MediaBookmarks{},
|
|
},
|
|
"POST /api/media-items/:id/bookmarks": {
|
|
Request: handlers.CreateMediaBookmarkRequest{},
|
|
Response: database.MediaBookmarks{},
|
|
},
|
|
"PUT /api/media-items/:id/bookmarks/:bookmarkId": {
|
|
Request: handlers.UpdateMediaBookmarkRequest{},
|
|
Response: database.MediaBookmarks{},
|
|
},
|
|
"DELETE /api/media-items/:id/bookmarks/:bookmarkId": {},
|
|
// Review: the upsert binds an inline anonymous struct {review}; pinned
|
|
// by RawMessage. Empty review string = delete, returns {"review": null}.
|
|
"POST /api/media-items/:id/review": {
|
|
Request: json.RawMessage(`{"review": ""}`),
|
|
Response: database.MediaReviews{},
|
|
},
|
|
"PUT /api/media-items/:id/review": {
|
|
Request: json.RawMessage(`{"review": ""}`),
|
|
Response: database.MediaReviews{},
|
|
},
|
|
"GET /api/media-items/:id/review": {
|
|
Response: database.MediaReviews{},
|
|
},
|
|
"DELETE /api/media-items/:id/review": {
|
|
Response: json.RawMessage(`{"message": ""}`),
|
|
},
|
|
"GET /api/media-items/:id/notes/:noteId": {
|
|
Response: database.MediaNotes{},
|
|
},
|
|
"GET /api/media-items/:id/highlights/:highlightId": {
|
|
Response: database.MediaHighlights{},
|
|
},
|
|
"POST /api/media-items/:id/annotations/:annotationId/restore": {
|
|
Request: nil, // kind via ?annotation_type= or optional body
|
|
Response: json.RawMessage(`{"restored": false}`),
|
|
},
|
|
"DELETE /api/media-items/:id/annotations/:annotationId": {
|
|
Request: nil, // kind via required ?annotation_type=
|
|
Response: json.RawMessage(`{"purged": false}`),
|
|
},
|
|
|
|
// --- media-items batch 3b: management ---
|
|
"GET /api/media-items/:id/download": {
|
|
// Serves raw file bytes, not JSON — no examples by design.
|
|
},
|
|
"PUT /api/media-items/:id/audio-speed": {
|
|
Request: json.RawMessage(`{"speed": 0}`),
|
|
Response: json.RawMessage(`{"playback_speed": 0}`),
|
|
},
|
|
"POST /api/media-items/:id/rescan": {
|
|
// Returns the FULL media_items row (70+ columns); pin is an abridged
|
|
// key subset — the doc example declares the same subset and says so.
|
|
Response: json.RawMessage(`{
|
|
"id": "", "library_id": "", "title": "", "author": "",
|
|
"file_path": "", "mime_type": "", "scan_status": "", "updated_at": ""
|
|
}`),
|
|
},
|
|
"POST /api/media-items/purge-archived": {
|
|
Response: json.RawMessage(`{"purged": 0}`),
|
|
},
|
|
"POST /api/media-items/:id/unarchive": {},
|
|
"POST /api/media-items/:id/merge": {
|
|
Request: json.RawMessage(`{"target_id": ""}`),
|
|
Response: json.RawMessage(`{"message": ""}`),
|
|
},
|
|
"GET /api/media-items/facets": {
|
|
Response: json.RawMessage(`{"facet": "", "values": [{}]}`),
|
|
},
|
|
"GET /api/media-items/filter-count": {
|
|
Response: json.RawMessage(`{"count": 0}`),
|
|
},
|
|
|
|
// --- analytics + jobs (batch 4) ---
|
|
"GET /api/analytics/device-usage": {
|
|
Response: handlers.DeviceUsageResponse{},
|
|
},
|
|
"GET /api/analytics/popular-books": {
|
|
Response: handlers.PopularBooksResponse{},
|
|
},
|
|
"POST /api/jobs": {
|
|
// Inline anonymous request struct {type, params} — pinned by sample.
|
|
Request: json.RawMessage(`{"type": "", "params": {}}`),
|
|
Response: json.RawMessage(`{"message": "", "job_id": "", "type": "", "status": ""}`),
|
|
},
|
|
"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
|
|
}`),
|
|
},
|
|
|
|
// --- Beam batch 2: conflicts + queue backfill ---
|
|
"GET /api/conflicts": {
|
|
Response: handlers.ConflictListResponse{},
|
|
},
|
|
"GET /api/conflicts/:id": {
|
|
Response: handlers.ConflictDetailResponse{},
|
|
},
|
|
"DELETE /api/conflicts/:id": {},
|
|
"POST /api/conflicts/:id/resolve": {
|
|
Request: handlers.ConflictResolutionRequest{},
|
|
Response: handlers.ConflictResolveResponse{},
|
|
},
|
|
"POST /api/conflicts/dismiss-all": {
|
|
// deleted is an INT count of dismissed rows.
|
|
Response: json.RawMessage(`{"deleted": 0}`),
|
|
},
|
|
"POST /api/conflicts/bulk-resolve": {
|
|
Request: handlers.BulkResolveRequest{},
|
|
Response: handlers.BulkResolveResponse{},
|
|
},
|
|
"POST /api/conflicts/bulk-dismiss": {
|
|
// Inline anonymous request struct {conflict_ids} — pinned by sample.
|
|
Request: json.RawMessage(`{"conflict_ids": []}`),
|
|
Response: handlers.BulkResolveResponse{},
|
|
},
|
|
|
|
// NOTE: queue param names are :device_id / :item_id in the router —
|
|
// first registry draft used :id and the gate caught it.
|
|
"GET /api/queue/devices/:device_id/stats": {
|
|
Response: handlers.QueueStatsResponse{},
|
|
},
|
|
"GET /api/queue/devices/:device_id/items": {
|
|
Response: json.RawMessage(`{"items": [{}], "count": 0}`),
|
|
},
|
|
"GET /api/queue/items": {
|
|
Response: json.RawMessage(`{"items": [{}], "count": 0}`),
|
|
},
|
|
"POST /api/queue/items/:item_id/retry": {
|
|
Response: json.RawMessage(`{
|
|
"message": "", "item_id": "", "status": "",
|
|
"attempts": 0, "max_attempts": 0
|
|
}`),
|
|
},
|
|
"DELETE /api/queue/items/:item_id": {},
|
|
"DELETE /api/queue/devices/:device_id/clear": {
|
|
Response: json.RawMessage(`{"message": ""}`),
|
|
},
|
|
|
|
// --- Beam batch 3: auth core + devices core ---
|
|
"POST /api/auth/login": {
|
|
Request: handlers.LoginRequest{},
|
|
Response: handlers.AuthResponse{},
|
|
},
|
|
"POST /api/auth/register": {
|
|
Request: handlers.RegisterRequest{},
|
|
Response: handlers.AuthResponse{},
|
|
},
|
|
"POST /api/auth/refresh": {
|
|
Request: handlers.RefreshTokenRequest{},
|
|
Response: handlers.RefreshTokenResponse{},
|
|
},
|
|
"POST /api/auth/logout": {
|
|
Response: json.RawMessage(`{"message": ""}`),
|
|
},
|
|
"GET /api/auth/profile": {
|
|
Response: handlers.UserProfile{},
|
|
},
|
|
"GET /api/devices": {
|
|
Response: handlers.DeviceListResponse{},
|
|
},
|
|
"PUT /api/devices/:id": {
|
|
Request: handlers.DeviceUpdateRequest{},
|
|
Response: json.RawMessage(`{
|
|
"device_updated": false,
|
|
"device": {
|
|
"id": "", "device_name": "", "device_type": "",
|
|
"last_sync": null, "last_seen": null,
|
|
"sync_enabled": false, "auto_sync": false,
|
|
"sync_frequency_minutes": 0, "created_at": "",
|
|
"device_metadata": null, "auth_token": ""
|
|
}
|
|
}`),
|
|
},
|
|
"DELETE /api/devices/:id": {},
|
|
"GET /api/devices/:id/shelves": {
|
|
Response: json.RawMessage(`{
|
|
"status": "", "device_id": "", "shelf_name": "",
|
|
"books": [{}], "total": 0
|
|
}`),
|
|
},
|
|
"POST /api/devices/:id/shelves": {
|
|
Request: handlers.AddToShelfRequest{},
|
|
Response: json.RawMessage(`{
|
|
"status": "", "added_count": 0,
|
|
"shelf_name": "", "total_count": 0
|
|
}`),
|
|
},
|
|
"DELETE /api/devices/:id/shelves": {
|
|
Response: json.RawMessage(`{"status": "", "message": ""}`),
|
|
},
|
|
"DELETE /api/devices/:id/shelves/clear": {
|
|
Response: json.RawMessage(`{"status": "", "message": ""}`),
|
|
},
|
|
"GET /api/devices/:id/sidecar": {
|
|
Response: handlers.SidecarConfig{},
|
|
},
|
|
"POST /api/devices/register": {
|
|
Request: handlers.DeviceRegistrationRequest{},
|
|
Response: handlers.DeviceRegistrationResponse{},
|
|
},
|
|
"POST /api/devices/register/status": {
|
|
Request: handlers.DeviceAuthStatusRequest{},
|
|
Response: handlers.DeviceAuthStatusResponse{},
|
|
},
|
|
|
|
// --- Beam batch 4: media-items core ---
|
|
"GET /api/media-items": {
|
|
Response: json.RawMessage(`{"data": [{}]}`),
|
|
},
|
|
"GET /api/media-items/:id": {
|
|
// Hand-built ~44-key map — pinned verbatim from the handler.
|
|
Response: json.RawMessage(`{
|
|
"id": "", "library_id": "", "title": "", "subtitle": "",
|
|
"author": "", "authors": [], "isbn": "", "description": "",
|
|
"file_path": "", "file_size": 0, "mime_type": "",
|
|
"cover_image_path": "", "series": "", "series_number": 0,
|
|
"tags": [], "asin": "", "date_published": "", "publisher": "",
|
|
"publishers": [], "explicit": false, "contributors": [],
|
|
"language": "", "edition": "", "page_count": 0, "genre": "",
|
|
"created_at": "", "updated_at": "", "format_group": "",
|
|
"format_mimetype": "", "narrators": [], "abridged": false,
|
|
"manga_type": "", "reading_direction": "", "series_count": 0,
|
|
"volume": 0, "imprint": "", "age_rating": "", "web_url": "",
|
|
"metadata_notes": "", "community_rating": 0, "story_arc": "",
|
|
"is_black_and_white": false, "alternate_info": "",
|
|
"scan_information": "", "summary": "", "library_type_name": ""
|
|
}`),
|
|
},
|
|
"POST /api/media-items": {
|
|
Request: handlers.CreateMediaItemRequest{},
|
|
Response: database.MediaItems{},
|
|
},
|
|
"PUT /api/media-items/:id": {
|
|
Request: handlers.UpdateMediaItemRequest{},
|
|
Response: database.MediaItems{},
|
|
},
|
|
"DELETE /api/media-items/:id": {},
|
|
"GET /api/media-items/search": {
|
|
Response: []database.SearchMediaItemsRow{},
|
|
},
|
|
"POST /api/media-items/:id/rating": {
|
|
// NOTE: rating scale is 1-10 int (odd = half-star).
|
|
Request: json.RawMessage(`{"rating": 0}`),
|
|
Response: database.MediaRatings{},
|
|
},
|
|
"PUT /api/media-items/:id/rating": {
|
|
Request: json.RawMessage(`{"rating": 0}`),
|
|
Response: database.MediaRatings{},
|
|
},
|
|
"GET /api/media-items/:id/rating": {
|
|
Response: database.MediaRatings{}, // absent -> {"rating": null}
|
|
},
|
|
"DELETE /api/media-items/:id/rating": {
|
|
Response: json.RawMessage(`{"message": ""}`),
|
|
},
|
|
"GET /api/media-items/:id/progress": {
|
|
Response: json.RawMessage(`{
|
|
"id": null, "media_item_id": null, "user_id": null,
|
|
"current_page": 0, "total_pages": null, "last_read_at": null,
|
|
"percentage": null, "character_offset": null, "epubcfi": null,
|
|
"chapter": null, "chapter_progress": null, "format_group": null,
|
|
"total_characters": null, "chapter_count": null,
|
|
"last_sync_device": null, "last_sync_source": null,
|
|
"last_sync_timestamp": null, "audio_offset_seconds": null,
|
|
"reading_mode": null, "viewport_x": null, "viewport_y": null,
|
|
"zoom_level": null, "scroll_position_x": null,
|
|
"scroll_position_y": null, "panel_number": null,
|
|
"css_selector": null, "anchor_href": null,
|
|
"char_offset": null, "context_text": null
|
|
}`),
|
|
},
|
|
"PUT /api/media-items/:id/progress": {
|
|
Request: sync.SaveProgressRequest{},
|
|
Response: database.ReadingProgress{},
|
|
},
|
|
|
|
// --- Beam batch 5: libraries ---
|
|
"POST /api/libraries": {
|
|
Request: handlers.CreateLibraryRequest{},
|
|
Response: database.Libraries{},
|
|
},
|
|
"GET /api/libraries/:id": {
|
|
Response: database.Libraries{},
|
|
},
|
|
"PUT /api/libraries/:id": {
|
|
Request: handlers.UpdateLibraryRequest{},
|
|
Response: database.Libraries{},
|
|
},
|
|
"DELETE /api/libraries/:id": {},
|
|
"POST /api/libraries/:id/folders": {
|
|
Request: handlers.AddLibraryFolderRequest{},
|
|
Response: database.LibraryFolders{},
|
|
},
|
|
"DELETE /api/libraries/:id/folders": {
|
|
// Handler REUSES AddLibraryFolderRequest ({folder_path}) on delete.
|
|
Request: handlers.AddLibraryFolderRequest{},
|
|
},
|
|
"GET /api/libraries/visibility": {
|
|
Response: []database.GetUserVisibleLibrariesRow{},
|
|
},
|
|
"POST /api/libraries/visibility": {
|
|
Request: handlers.SetLibraryVisibilityRequest{},
|
|
Response: database.LibraryVisibility{},
|
|
},
|
|
"GET /api/libraries/browse": {
|
|
Response: json.RawMessage(`{
|
|
"current_path": "", "parent_path": "", "directories": [{}]
|
|
}`),
|
|
},
|
|
"GET /api/libraries/:id/stats": {
|
|
Response: services.LibraryStats{},
|
|
},
|
|
"GET /api/libraries/:id/media-items": {
|
|
// Delegates to ListMediaItems with library_id injected.
|
|
Response: json.RawMessage(`{"data": [{}]}`),
|
|
},
|
|
|
|
// --- Beam batch 6: kobo/koreader (device-token) + collections missing ---
|
|
"POST /api/sync/kobo/:token/markup": {
|
|
Request: AnyJSONObject{}, // kobo protocol payload (ReadingSync/BookmarkSync)
|
|
Response: handlers.KoboSyncStatus{}, // kobo firmware naming: Status/MarkupsSynced
|
|
},
|
|
"POST /api/sync/kobo/:token/bookmark": {
|
|
Request: AnyJSONObject{}, // kobo bookmark payload
|
|
Response: handlers.KoboSyncStatus{},
|
|
},
|
|
"POST /api/sync/kobo/:token/v1/analytics/gettests": {
|
|
Request: AnyJSONObject{}, // kobo analytics POST body
|
|
// A/B test payload — dynamic keys (tests/features/configuration).
|
|
Response: AnyJSONObject{},
|
|
},
|
|
"GET /api/sync/kobo/:token/v1/initialization": {
|
|
// NOTE: returns the library sync payload, not the init response.
|
|
Response: handlers.KoboLibraryResponse{},
|
|
},
|
|
"POST /api/sync/kobo/:token/sync-from-server": {
|
|
Request: AnyJSONObject{}, // kobo sync POST body
|
|
Response: handlers.KoboServerSyncResponse{},
|
|
},
|
|
"POST /api/sync/koreader/progress": {
|
|
Request: handlers.KOReaderProgressRequest{},
|
|
Response: handlers.KOReaderSyncResponse{},
|
|
},
|
|
"GET /api/sync/koreader/resolve": {
|
|
Response: handlers.KOReaderResolveResponse{},
|
|
},
|
|
"GET /api/sync/koreader/metadata/:uuid": {
|
|
// Hand-built map in the handler.
|
|
Response: AnyJSONObject{},
|
|
},
|
|
"GET /api/sync/koreader/library": {
|
|
Response: handlers.KOReaderLibraryResponse{},
|
|
},
|
|
"POST /api/sync/koreader/bookmarks": {
|
|
Request: AnyJSONObject{}, // {highlights: [...]} inline struct in handler
|
|
Response: AnyJSONObject{}, // hand-built map in the handler
|
|
},
|
|
"PUT /api/collections/:id/items/order": {
|
|
// Inline request {item_ids} — pinned by sample.
|
|
Request: json.RawMessage(`{"item_ids": []}`),
|
|
Response: json.RawMessage(`{"status": "reordered"}`),
|
|
},
|
|
"PUT /api/collections/:id/rules/:ruleId": {
|
|
// Handler is SetRuleEnabled: toggle only, request {enabled required},
|
|
// response {collection_id, is_smart}.
|
|
Request: json.RawMessage(`{"enabled": false}`),
|
|
Response: json.RawMessage(`{
|
|
"collection_id": "", "is_smart": false
|
|
}`),
|
|
},
|
|
|
|
// --- Beam batch 7: scanner ---
|
|
"POST /api/scanner/scan": {
|
|
Request: handlers.ScanLibraryRequest{},
|
|
// 202 Accepted; 409 coalescing variant adds {active_job_id}.
|
|
Response: json.RawMessage(`{
|
|
"message": "", "job_id": "", "status": ""
|
|
}`),
|
|
},
|
|
"POST /api/scanner/start": {
|
|
// Starts the long-lived watch scanner over folder_paths.
|
|
Request: handlers.ScanLibraryRequest{},
|
|
Response: json.RawMessage(`{"message": ""}`),
|
|
},
|
|
"POST /api/scanner/stop": {
|
|
Response: json.RawMessage(`{"message": ""}`),
|
|
},
|
|
"GET /api/scanner/status/:jobId": {
|
|
// result is interface{} (job payload). 404 {error} when unknown job.
|
|
Response: json.RawMessage(`{
|
|
"job_id": "", "status": "", "error": "", "result": null,
|
|
"progress": 0, "files_scanned": 0, "new_items": 0, "errors": 0
|
|
}`),
|
|
},
|
|
"GET /api/scanner/active": {
|
|
// jobs are []JobResult (job_id/status/progress/files_scanned/...).
|
|
Response: json.RawMessage(`{
|
|
"scanning": false, "jobs": []
|
|
}`),
|
|
},
|
|
"GET /api/scanner/active-summary": {
|
|
Response: json.RawMessage(`{
|
|
"scanning": false, "progress": 0
|
|
}`),
|
|
},
|
|
"POST /api/scanner/cancel/:jobId": {
|
|
Response: json.RawMessage(`{
|
|
"message": "", "job_id": "", "status": ""
|
|
}`),
|
|
},
|
|
"POST /api/scanner/watch/start": {
|
|
Request: json.RawMessage(`{"library_id": ""}`),
|
|
Response: json.RawMessage(`{"message": "", "library_id": ""}`),
|
|
},
|
|
"POST /api/scanner/watch/stop": {
|
|
Request: json.RawMessage(`{"library_id": ""}`),
|
|
Response: json.RawMessage(`{"message": "", "library_id": ""}`),
|
|
},
|
|
"GET /api/scanner/watch/status": {
|
|
Response: json.RawMessage(`{
|
|
"watching_libraries": [""], "total_watching": 0
|
|
}`),
|
|
},
|
|
|
|
// --- Beam batch 8: devices remaining + auth remainder ---
|
|
"GET /api/devices/pending": {
|
|
// Device registrations live in an in-memory map; both key spellings
|
|
// are emitted for compatibility.
|
|
Response: json.RawMessage(`{
|
|
"registrations": [], "pending_registrations": [], "total": 0
|
|
}`),
|
|
},
|
|
"GET /api/devices/approve/:registration_id": {
|
|
// Already-approved variant omits registration_id. Credentials ride
|
|
// the registration map polled elsewhere — NOT this JSON body.
|
|
Response: json.RawMessage(`{
|
|
"message": "", "device_name": "", "device_type": "",
|
|
"registration_id": "", "approved": false
|
|
}`),
|
|
},
|
|
"POST /api/devices/reject/:registration_id": {
|
|
Response: json.RawMessage(`{"message": ""}`),
|
|
},
|
|
"POST /api/auth/users": {
|
|
Request: handlers.RegisterRequest{},
|
|
Response: json.RawMessage(`{
|
|
"id": "", "email": "", "username": "", "role": "",
|
|
"timezone": "", "created_at": null
|
|
}`),
|
|
},
|
|
"GET /api/auth/users": {
|
|
// sqlc join row (device_count), NOT database.Users — no password_hash
|
|
// on the wire (live-smoke-verified 2026-10-24).
|
|
Response: []database.ListUsersRow{},
|
|
},
|
|
"PUT /api/auth/users/:id/max-devices": {
|
|
Request: handlers.UpdateUserMaxDevicesRequest{},
|
|
Response: json.RawMessage(`{"message": ""}`),
|
|
},
|
|
"PUT /api/auth/password": {
|
|
// Self-change (jwt). Admin variant PUT /auth/password/:id documented
|
|
// in prose; handler is shared (admin mode skips current_password).
|
|
Request: json.RawMessage(`{
|
|
"current_password": "", "new_password": "", "confirm_password": ""
|
|
}`),
|
|
Response: json.RawMessage(`{"message": ""}`),
|
|
},
|
|
"PUT /api/auth/profile": {
|
|
// Shared with admin variant (PUT /auth/profile/:id).
|
|
Request: handlers.ProfileUpdateRequest{},
|
|
Response: json.RawMessage(`{"message": ""}`),
|
|
},
|
|
"DELETE /api/auth/profile": {
|
|
// Self-delete; admin variant DELETE /auth/profile/:id (reassignment
|
|
// of admin-owned assets to another admin before cascade).
|
|
Response: json.RawMessage(`{"message": ""}`),
|
|
},
|
|
"GET /api/progress/:id/history": {
|
|
// id is MEDIA ITEM id; sessions are ReadingHistory rows.
|
|
Response: json.RawMessage(`{
|
|
"media_item_id": "", "sessions": [], "count": 0
|
|
}`),
|
|
},
|
|
"GET /api/books/match": {
|
|
Response: services.BookQueryResponse{},
|
|
},
|
|
"GET /api/auth/password-rules": {
|
|
Response: json.RawMessage(`{
|
|
"min_length": 8, "require_upper": true, "require_lower": true,
|
|
"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": {},
|
|
|
|
// --- Beam batch 10: book-matching + collections remainder ---
|
|
"POST /api/sync/books/query": {
|
|
// Plugin/sync-protocol variant of GET /api/books/match (body, not
|
|
// query params; unfiltered legacy path).
|
|
Request: services.BookQueryRequest{},
|
|
Response: services.BookQueryResponse{},
|
|
},
|
|
"POST /api/sync/bulk-link-books": {
|
|
Request: handlers.BulkLinkBooksRequest{},
|
|
Response: json.RawMessage(`{
|
|
"results": [], "total": 0, "successful": 0, "failed": 0
|
|
}`),
|
|
},
|
|
"POST /api/sync/auto-link-books": {
|
|
Request: handlers.AutoLinkBooksRequest{},
|
|
Response: json.RawMessage(`{
|
|
"auto_linked": 0, "results": []
|
|
}`),
|
|
},
|
|
"GET /api/devices/:deviceId/sync/unlinked-books": {
|
|
Response: json.RawMessage(`{
|
|
"unlinked": [], "total": 0
|
|
}`),
|
|
},
|
|
"GET /api/sync/unlinked-books/:id/suggestions": {
|
|
Response: json.RawMessage(`{
|
|
"unlinked_book_id": "", "title_from_device": "", "sha256": "",
|
|
"suggestions": [], "total_suggestions": 0, "action": ""
|
|
}`),
|
|
},
|
|
"GET /api/devices/:id/file-aliases": {
|
|
Response: json.RawMessage(`{
|
|
"device_id": "", "aliases": [], "total": 0
|
|
}`),
|
|
},
|
|
"POST /api/devices/:id/file-aliases": {
|
|
// Inline request struct in handler.
|
|
Request: json.RawMessage(`{
|
|
"media_item_id": "", "file_path": "", "file_sha256": "",
|
|
"confidence_score": 0
|
|
}`),
|
|
Response: json.RawMessage(`{
|
|
"id": "", "media_item_id": "", "device_id": "", "file_path": "",
|
|
"file_sha256": "", "confidence_score": 0, "last_seen_at": ""
|
|
}`),
|
|
},
|
|
"PUT /api/devices/:id/file-aliases/:aliasId": {
|
|
// Partial update — pointer fields.
|
|
Request: json.RawMessage(`{
|
|
"media_item_id": "", "file_sha256": "", "confidence_score": 0
|
|
}`),
|
|
Response: json.RawMessage(`{
|
|
"id": "", "media_item_id": "", "file_path": "",
|
|
"file_sha256": "", "confidence_score": 0, "last_seen_at": ""
|
|
}`),
|
|
},
|
|
"DELETE /api/devices/:id/file-aliases/:aliasId": {
|
|
Response: json.RawMessage(`{"message": ""}`),
|
|
},
|
|
"POST /api/collections/:id/rules": {
|
|
// First rule converts a static collection to smart.
|
|
Request: handlers.AddRuleRequest{},
|
|
Response: json.RawMessage(`{
|
|
"collection_id": "", "rule": {}, "is_smart": false
|
|
}`),
|
|
},
|
|
"DELETE /api/collections/:id/rules/:ruleId": {
|
|
Response: json.RawMessage(`{
|
|
"collection_id": "", "is_smart": false
|
|
}`),
|
|
},
|
|
"POST /api/collections/test-rules": {
|
|
Request: handlers.TestRulesRequest{},
|
|
Response: json.RawMessage(`{"matches": [], "total": 0}`),
|
|
},
|
|
"POST /api/collections/:id/books": {
|
|
// Bulk add: {operations: [{collection_id, book_ids:[...]}]}.
|
|
Request: json.RawMessage(`{
|
|
"operations": [{"collection_id": "", "book_ids": []}]
|
|
}`),
|
|
Response: json.RawMessage(`{"results": [], "total": 0}`),
|
|
},
|
|
"POST /api/devices/:id/collections": {
|
|
Request: handlers.CreateDeviceMappingRequest{},
|
|
Response: json.RawMessage(`{
|
|
"id": "", "collection_id": "", "device_id": "",
|
|
"device_shelf_name": "", "sync_direction": "", "created_at": ""
|
|
}`),
|
|
},
|
|
|
|
// --- Beam batch 11: final backfill ---
|
|
"GET /api/collections/:id/books": {
|
|
// Reverse listing: which collections carry this book.
|
|
Response: json.RawMessage(`{
|
|
"book_id": "", "collections": [], "total": 0
|
|
}`),
|
|
},
|
|
"POST /api/collections/:id/books/bulk-remove": {
|
|
Request: handlers.BulkRemoveBooksRequest{},
|
|
Response: json.RawMessage(`{"removed": 0, "total": 0}`),
|
|
},
|
|
"POST /api/collections/bulk-add-books": {
|
|
Request: json.RawMessage(`{
|
|
"operations": [{"collection_id": "", "book_ids": []}]
|
|
}`),
|
|
Response: json.RawMessage(`{"results": [], "total": 0}`),
|
|
},
|
|
"POST /api/collections/preview": {
|
|
// Smart-collection dry run — inline request struct.
|
|
Request: json.RawMessage(`{
|
|
"library_id": "", "rules": [], "manual_book_ids": [], "limit": 0
|
|
}`),
|
|
Response: json.RawMessage(`{"items": []}`),
|
|
},
|
|
"DELETE /api/devices/:id/collections/:collectionId": {
|
|
// 204 No Content.
|
|
},
|
|
"POST /api/media-items/bulk-delete": {
|
|
Request: json.RawMessage(`{"media_item_ids": []}`),
|
|
Response: json.RawMessage(`{
|
|
"results": [], "total": 0, "deleted": 0, "failed": 0
|
|
}`),
|
|
},
|
|
"POST /api/media-items/bulk-update": {
|
|
// updates fields are pointers (partial per-item patch).
|
|
Request: json.RawMessage(`{
|
|
"media_item_updates": [
|
|
{
|
|
"media_item_id": "",
|
|
"updates": {
|
|
"title": "", "author": "", "genre": "",
|
|
"language": "", "tags": []
|
|
}
|
|
}
|
|
]
|
|
}`),
|
|
Response: json.RawMessage(`{
|
|
"results": [], "total": 0, "updated": 0, "failed": 0
|
|
}`),
|
|
},
|
|
"GET /api/media-items/:id/annotations/deleted": {
|
|
Response: json.RawMessage(`{
|
|
"deleted_annotations": [], "total": 0
|
|
}`),
|
|
},
|
|
"GET /api/media-items/:id/chapters": {
|
|
Response: json.RawMessage(`{
|
|
"media_item_id": "", "chapters": [], "duration_seconds": 0,
|
|
"codec_profile": {}, "layout": "", "tracks": []
|
|
}`),
|
|
},
|
|
"GET /api/media-items/:id/audio/:trackIndex": {
|
|
// Raw audio bytes via http.ServeContent (Range support) — not JSON.
|
|
},
|
|
"GET /api/sync/koreader/rating": {
|
|
// Book identity via query params (uuid/sha256/title/authors).
|
|
Response: json.RawMessage(`{"rating": null}`),
|
|
},
|
|
"POST /api/sync/koreader/rating": {
|
|
// koreaderBookRef embedded + rating 0-10 (odd = half-star).
|
|
Request: json.RawMessage(`{
|
|
"uuid": "", "sha256": "", "title": "", "authors": [], "rating": 0
|
|
}`),
|
|
Response: json.RawMessage(`{"rating": null}`),
|
|
},
|
|
"GET /api/sync/koreader/review": {
|
|
Response: json.RawMessage(`{"review": null}`),
|
|
},
|
|
"POST /api/sync/koreader/review": {
|
|
Request: json.RawMessage(`{
|
|
"uuid": "", "sha256": "", "title": "", "authors": [], "review": ""
|
|
}`),
|
|
Response: json.RawMessage(`{"review": null}`),
|
|
},
|
|
"POST /api/sync/koreader/stats": {
|
|
Request: json.RawMessage(`{
|
|
"uuid": "", "sha256": "", "title": "", "authors": [],
|
|
"total_seconds": 0
|
|
}`),
|
|
Response: AnyJSONObject{}, // hand-built result map
|
|
},
|
|
"GET /api/admin/hash-conflicts": {
|
|
// JSON list endpoint (admin). The resolve POST is an htmx fragment
|
|
// 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{},
|
|
},
|
|
}
|