Add sync conflict detection and resolution system
Implement conflict detection for concurrent reading progress updates from different devices. Adds conflict management endpoints for listing, viewing, and resolving conflicts. - Add ConflictHandler with CRUD endpoints for conflict management - Implement automatic conflict detection in KOReader progress updates - Add WebSocket broadcast for real-time conflict notifications - Add database query for listing user conflicts by status - Add integration tests and Bruno API test collection
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
meta {
|
||||
name: Delete Conflict
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
delete {
|
||||
url: {{baseUrl}}/api/conflicts/{{conflict_id}}
|
||||
body: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
headers: {
|
||||
Authorization: Bearer {{token}}
|
||||
}
|
||||
|
||||
docs: {
|
||||
Deletes a specific conflict record.
|
||||
|
||||
Path Parameters:
|
||||
- conflict_id: UUID of the conflict to delete
|
||||
|
||||
Use this when:
|
||||
- A conflict was created in error
|
||||
- You want to dismiss a conflict without resolving it
|
||||
- The conflict is no longer relevant
|
||||
|
||||
Response: 204 No Content on success
|
||||
|
||||
Note: This permanently removes the conflict record.
|
||||
Consider resolving the conflict instead if you want to
|
||||
maintain an audit trail of what happened.
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
meta {
|
||||
name: Dismiss All Resolved Conflicts
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/api/conflicts/dismiss-all
|
||||
body: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
headers: {
|
||||
Authorization: Bearer {{token}}
|
||||
}
|
||||
|
||||
docs: {
|
||||
Deletes all resolved conflicts for the authenticated user.
|
||||
|
||||
Use this to:
|
||||
- Clean up your conflicts list after reviewing resolutions
|
||||
- Remove old resolved conflicts that are no longer needed
|
||||
- Maintain a clean conflict history
|
||||
|
||||
Response includes:
|
||||
- deleted: Number of conflict records that were deleted
|
||||
|
||||
Example response:
|
||||
{
|
||||
"deleted": 5
|
||||
}
|
||||
|
||||
Note: Only resolves conflicts with status "user_resolved"
|
||||
are deleted. Unresolved conflicts are preserved.
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
meta {
|
||||
name: Get Conflict Details
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{baseUrl}}/api/conflicts/{{conflict_id}}
|
||||
body: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
headers: {
|
||||
Authorization: Bearer {{token}}
|
||||
}
|
||||
|
||||
docs: {
|
||||
Retrieves detailed information about a specific conflict.
|
||||
|
||||
Path Parameters:
|
||||
- conflict_id: UUID of the conflict
|
||||
|
||||
Response includes:
|
||||
- id: Conflict UUID
|
||||
- media_item_id: Associated book UUID
|
||||
- media_item_title: Book title
|
||||
- conflict_type: Type of conflict
|
||||
- conflict_data: Side-by-side comparison with sources:
|
||||
* source: Device/source identifier (koreader, kobo, web, etc.)
|
||||
* timestamp: When this progress was recorded
|
||||
* data: The conflicting data (percentage, epubcfi, chapter, etc.)
|
||||
- resolution_status: Current status
|
||||
- resolution_data: If resolved, includes resolution details
|
||||
- resolved_by: User ID who resolved it (if applicable)
|
||||
- resolved_at: When it was resolved (if applicable)
|
||||
- created_at: When conflict was detected
|
||||
|
||||
Example conflict_data:
|
||||
{
|
||||
"koreader": {
|
||||
"source": "koreader",
|
||||
"timestamp": "2026-01-30T20:10:00Z",
|
||||
"data": {
|
||||
"percentage": 0.45,
|
||||
"epubcfi": "epubcfi(/6/4/2:15)",
|
||||
"chapter": 3
|
||||
}
|
||||
},
|
||||
"kobo": {
|
||||
"source": "kobo",
|
||||
"timestamp": "2026-01-30T20:05:00Z",
|
||||
"data": {
|
||||
"percentage": 0.42,
|
||||
"location": "unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
meta {
|
||||
name: List Conflicts
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{baseUrl}}/api/conflicts?status=unresolved
|
||||
body: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
headers: {
|
||||
Authorization: Bearer {{token}}
|
||||
}
|
||||
|
||||
docs: {
|
||||
Lists all sync conflicts for the authenticated user.
|
||||
|
||||
Query Parameters:
|
||||
- status: Filter by resolution status (unresolved, user_resolved, auto_resolved, all)
|
||||
|
||||
Response includes:
|
||||
- conflicts: Array of conflict details
|
||||
- total: Total number of conflicts
|
||||
- unresolved: Number of unresolved conflicts
|
||||
|
||||
Each conflict includes:
|
||||
- id: Conflict UUID
|
||||
- media_item_id: Associated book UUID
|
||||
- media_item_title: Book title
|
||||
- conflict_type: Type of conflict (progress, note, highlight)
|
||||
- conflict_data: Side-by-side comparison of conflicting data
|
||||
- resolution_status: Current status
|
||||
- created_at: When conflict was detected
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
meta {
|
||||
name: Resolve Conflict
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/api/conflicts/{{conflict_id}}/resolve
|
||||
body: json
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
headers: {
|
||||
Authorization: Bearer {{token}}
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"winner": "koreader",
|
||||
"manual_data": null,
|
||||
"apply_to_all_future_conflicts": false,
|
||||
"reason": "User chose more recent progress"
|
||||
}
|
||||
}
|
||||
|
||||
docs: {
|
||||
Resolves a sync conflict by choosing which source to use.
|
||||
|
||||
Path Parameters:
|
||||
- conflict_id: UUID of the conflict to resolve
|
||||
|
||||
Request Body:
|
||||
- winner: Source to choose (koreader, kobo, web, manual)
|
||||
- manual_data: Required if winner is "manual" - contains the merged data
|
||||
- apply_to_all_future_conflicts: Whether to auto-resolve future conflicts from this source
|
||||
- reason: Optional explanation for the resolution
|
||||
|
||||
Example request body for choosing koreader:
|
||||
{
|
||||
"winner": "koreader",
|
||||
"reason": "More recent progress"
|
||||
}
|
||||
|
||||
Example request body for manual resolution:
|
||||
{
|
||||
"winner": "manual",
|
||||
"manual_data": {
|
||||
"percentage": 0.43,
|
||||
"epubcfi": "epubcfi(/6/4/2:20)",
|
||||
"chapter": 3
|
||||
},
|
||||
"reason": "Custom merged position"
|
||||
}
|
||||
|
||||
Response includes:
|
||||
- conflict_resolved: true if successful
|
||||
- applied_to: What was updated (progress, annotations)
|
||||
- devices_synced: List of device IDs that were notified
|
||||
|
||||
After resolution:
|
||||
- The winning data is applied to the reading progress
|
||||
- All connected devices are notified via WebSocket
|
||||
- Conflict status changes to "user_resolved"
|
||||
- Resolution data is stored for audit trail
|
||||
}
|
||||
Reference in New Issue
Block a user