Introduces a centralized ProgressService that handles all reading progress
writes across web, KOReader, and Kobo clients. The service implements:
- Merge strategy: reads existing progress first, then only overwrites
non-nil fields from the incoming request. This fixes the data loss bug
where partial updates (e.g., Kobo last-read-place sending only epubcfi
and chapter) would NULL out percentage, character_offset, etc.
- Enrichment: computes missing fields from available data:
- character_offset from percentage + total_characters
- current_page from percentage + total_pages
- percentage from current_page + total_pages (reverse)
- percentage from character_offset + total_characters (reverse)
- Conflict detection: when a different source writes progress within 5
minutes with >1% difference, records a sync_conflicts row and broadcasts
a WebSocket notification for real-time UI alerts.
- Broadcast control: SaveProgressRequest.Broadcast flag lets Kobo
last-read-place and SyncFromServer skip WebSocket broadcasts.
- Pointer fields on SaveProgressRequest: nil means preserve existing,
non-nil means overwrite. Eliminates ambiguity between zero values
and not-provided fields.
Also adds unit tests for buildProgressSnapshot helper function.