refactor(progress): SaveProgressRequest becomes the canonical PUT payload; GET serializes from the row
Batch H1 steps 1-2 of the hardening plan. Wire contracts frozen; the pinned shapes are unchanged modulo the whitelisted additive GET keys (docs/contract_pins.md). 1. One canonical request struct. The web PUT binds SaveProgressRequest directly (identity/attribution fields are json:"-" so they stay server-owned) and copies its server fields once — the hand-rolled DTO and the 20-line field-copy block are gone. Parse set for existing clients is IDENTICAL to the old DTO (verified in web/src: the ebook reader sends percentage/current_page/total_pages/reading_mode/epubcfi/ context_text/chapter/zoom_level — reader.ts:3113-3143; the audiobook reader sends percentage/chapter/audio_offset_seconds/elapsed_seconds/ reading_mode — audio.ts:518-520). 2. GET /api/media-items/:id/progress now serializes through one row-struct-driven builder instead of a hand-map, so a row column cannot be silently forgotten again (the audio_offset_seconds omission). Existing keys are verbatim; the row already carried audio_offset_seconds/reading_mode/viewport_x-y/zoom_level/ scroll_position_x-y/panel_number and they surface as WHITELISTED additive keys. The web audiobook reader already reads audio_offset_seconds (audio.ts:219) — with the key absent, durationless books resumed at chapter start (offset always 0); they now resume correctly. The conditional context_text bolt-on and the server-derived css_selector/anchor_href block are unchanged — restore precedence stays percentage-first, never re-derived.
This commit is contained in:
1 parent
a2ea34232c
commit
28bd323505
3 files changed
+93
-98
No files matched your search
+29
-25
@@ -294,39 +294,43 @@ func NewProgressService(db *database.Queries, connManager *ConnectionManager) *P
|
||||
return &ProgressService{db: db, connManager: connManager}
|
||||
}
|
||||
|
||||
// SaveProgressRequest is the CANONICAL progress write payload — the web
|
||||
// PUT binds it directly (identity fields are json:"-": server-owned, never
|
||||
// client-settable) and every internal writer fills it programmatically.
|
||||
// Wire keys are frozen: only additive json names are allowed here.
|
||||
type SaveProgressRequest struct {
|
||||
MediaItemID pgtype.UUID
|
||||
UserID pgtype.UUID
|
||||
Source string
|
||||
DeviceID pgtype.UUID
|
||||
MediaItemID pgtype.UUID `json:"-"`
|
||||
UserID pgtype.UUID `json:"-"`
|
||||
Source string `json:"-"`
|
||||
DeviceID pgtype.UUID `json:"-"`
|
||||
|
||||
Percentage *float64
|
||||
Epubcfi *string
|
||||
ContextText *string
|
||||
CharacterOffset *int64
|
||||
Chapter *int
|
||||
ChapterProgress *float64
|
||||
CurrentPage *int
|
||||
TotalPages *int
|
||||
ViewportX *float64
|
||||
ViewportY *float64
|
||||
ZoomLevel *float64
|
||||
ScrollX *float64
|
||||
ScrollY *float64
|
||||
PanelNumber *int
|
||||
ReadingMode *string
|
||||
Percentage *float64 `json:"percentage"`
|
||||
Epubcfi *string `json:"epubcfi"`
|
||||
ContextText *string `json:"context_text"`
|
||||
CharacterOffset *int64 `json:"character_offset"`
|
||||
Chapter *int `json:"chapter"`
|
||||
ChapterProgress *float64 `json:"chapter_progress"`
|
||||
CurrentPage *int `json:"current_page"`
|
||||
TotalPages *int `json:"total_pages"`
|
||||
ViewportX *float64 `json:"viewport_x"`
|
||||
ViewportY *float64 `json:"viewport_y"`
|
||||
ZoomLevel *float64 `json:"zoom_level"`
|
||||
ScrollX *float64 `json:"scroll_position_x"`
|
||||
ScrollY *float64 `json:"scroll_position_y"`
|
||||
PanelNumber *int `json:"panel_number"`
|
||||
ReadingMode *string `json:"reading_mode"`
|
||||
// Audiobook locator (seconds into the current chapter)
|
||||
AudioOffsetSeconds *int
|
||||
AudioOffsetSeconds *int `json:"audio_offset_seconds"`
|
||||
|
||||
// AudioElapsedSeconds: the player's honest wall-clock PLAYING time
|
||||
// since the last push (audio pushes fire while playing; pauses don't
|
||||
// count). The capture prefers it over the inter-push delta, capped to
|
||||
// the same 2h attributed ceiling.
|
||||
AudioElapsedSeconds *int32
|
||||
AudioElapsedSeconds *int32 `json:"elapsed_seconds"`
|
||||
|
||||
DeviceType string
|
||||
DeviceName string
|
||||
Broadcast bool
|
||||
DeviceType string `json:"-"`
|
||||
DeviceName string `json:"-"`
|
||||
Broadcast bool `json:"-"`
|
||||
|
||||
// ClearLocator declares that this write INTENDS to carry no position
|
||||
// locator: percentage-only writers (devices whose converter declined,
|
||||
@@ -336,7 +340,7 @@ type SaveProgressRequest struct {
|
||||
// client-side far-anchor restore floor. Locater-less rows are honest:
|
||||
// percentage restores are approximate, and the next anchor-bearing
|
||||
// write re-establishes the pair. Request-supplied fields always win.
|
||||
ClearLocator bool
|
||||
ClearLocator bool `json:"-"`
|
||||
}
|
||||
|
||||
func (s *ProgressService) SaveProgress(ctx context.Context, req SaveProgressRequest) (database.ReadingProgress, error) {
|
||||
|
||||
Reference in new issue
Block a user