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:
2026-01-31 11:45:52 -05:00
parent 9b41b3ecb0
commit 2d2d643873
11 changed files with 1084 additions and 16 deletions
+21 -2
View File
@@ -59,6 +59,7 @@ func main() {
koreaderHandler := handlers.NewKOReaderHandler(queries, connManager)
wsHandler := handlers.NewWSHandler(queries, connManager, cfg.JWTSecret, deviceAuthMiddleware)
conflictHandler := handlers.NewConflictHandler(queries, connManager)
e := echo.New()
@@ -128,10 +129,20 @@ func main() {
// Public library types endpoint (no authentication required)
e.GET("/api/libraries/types", libraryHandler.GetLibraryTypes)
// Refresh token endpoint (no authentication required - uses refresh token from body)
e.POST("/api/auth/refresh", authHandler.RefreshAccessToken)
// Logout endpoint (optional authentication - can revoke tokens if provided)
e.POST("/api/auth/logout", authHandler.Logout)
// Protected routes
protected = e.Group("/api", jwtMiddleware)
// Setup ebook handler routes first (so we can use it for library scan)
h = handlers.SetupRoutes(protected, queries, connManager)
protected.GET("/auth/profile", authHandler.GetProfile)
protected.PUT("/auth/profile", authHandler.UpdateProfile)
protected.POST("/auth/refresh", authHandler.RefreshAccessToken)
protected.POST("/auth/logout", authHandler.Logout)
// Admin-only routes for user and folder management
admin := protected.Group("/auth", handlers.AdminMiddleware)
@@ -219,6 +230,14 @@ func main() {
devices.GET("/approve/:registration_id", deviceHandler.ApproveDevice)
devices.POST("/reject/:registration_id", deviceHandler.RejectDevice)
// Conflict resolution routes (protected - require user auth)
conflicts := protected.Group("/conflicts")
conflicts.GET("", conflictHandler.ListConflicts)
conflicts.GET("/:id", conflictHandler.GetConflict)
conflicts.POST("/:id/resolve", conflictHandler.ResolveConflict)
conflicts.DELETE("/:id", conflictHandler.DeleteConflict)
conflicts.POST("/dismiss-all", conflictHandler.DismissAllResolved)
// WebSocket endpoint for real-time sync
e.GET("/ws/sync", wsHandler.HandleWebSocket)