refactor(conflicts): one ConflictDetail builder for the three row shapes
ListConflicts, GetConflict, and the SSR media-detail active-conflict lookup each hand-mapped their sqlc row (list-by-user, get-by-id, list-by-media-item) into ConflictDetailResponse, triplicating the optional resolution tail — including the silently-swallowed ResolutionData unmarshal error, three times. NewConflictDetail is the single mapper; a malformed resolution blob now logs instead of vanishing. Also extracts koreaderSyncEndpoints (the KOReader URL map duplicated verbatim between token rotation and device approval; kobo's two URL sets stay local because they deliberately differ). Verified in the test-runner container (host conflict tests fail environmentally on the container-only uploads path — baseline behavior).
This commit is contained in:
1 parent
58fbe372e2
commit
b47a2b86a7
3 files changed
+62
-55
No files matched your search
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -103,26 +104,10 @@ func (h *ConflictHandler) GetConflictsData(c *echo.Context) ([]ConflictDetailRes
|
||||
continue
|
||||
}
|
||||
|
||||
detail := ConflictDetailResponse{
|
||||
ID: uuid.UUID(conflict.ID.Bytes).String(),
|
||||
MediaItemID: uuid.UUID(conflict.MediaItemID.Bytes).String(),
|
||||
MediaItemTitle: conflict.Title,
|
||||
ConflictType: conflict.ConflictType,
|
||||
ConflictData: conflictData,
|
||||
ResolutionStatus: conflict.ResolutionStatus.String,
|
||||
CreatedAt: conflict.CreatedAt.Time,
|
||||
}
|
||||
|
||||
if conflict.ResolvedBy.Valid {
|
||||
detail.ResolvedBy = uuid.UUID(conflict.ResolvedBy.Bytes).String()
|
||||
}
|
||||
if conflict.ResolvedAt.Valid {
|
||||
detail.ResolvedAt = &conflict.ResolvedAt.Time
|
||||
}
|
||||
if conflict.ResolutionData != nil {
|
||||
if err := json.Unmarshal(conflict.ResolutionData, &detail.ResolutionData); err == nil {
|
||||
}
|
||||
}
|
||||
detail := NewConflictDetail(conflict.ID, conflict.MediaItemID,
|
||||
conflict.Title, conflict.ConflictType, conflict.ResolutionStatus.String,
|
||||
conflictData, conflict.CreatedAt, conflict.ResolvedAt, conflict.ResolvedBy,
|
||||
conflict.ResolutionData)
|
||||
|
||||
response = append(response, detail)
|
||||
if conflict.ResolutionStatus.String == "unresolved" {
|
||||
@@ -133,6 +118,41 @@ func (h *ConflictHandler) GetConflictsData(c *echo.Context) ([]ConflictDetailRes
|
||||
return response, len(response), unresolvedCount, nil
|
||||
}
|
||||
|
||||
// NewConflictDetail maps any sync-conflict row shape (list-by-user,
|
||||
// get-by-id, list-by-media-item) to the wire detail. The resolution
|
||||
// tail is conditional exactly as before; a malformed resolution blob
|
||||
// now logs instead of vanishing silently.
|
||||
func NewConflictDetail(
|
||||
id, mediaItemID pgtype.UUID,
|
||||
title, conflictType, resolutionStatus string,
|
||||
conflictData map[string]ConflictSourceData,
|
||||
createdAt, resolvedAt pgtype.Timestamptz,
|
||||
resolvedBy pgtype.UUID,
|
||||
resolutionData []byte,
|
||||
) ConflictDetailResponse {
|
||||
detail := ConflictDetailResponse{
|
||||
ID: uuid.UUID(id.Bytes).String(),
|
||||
MediaItemID: uuid.UUID(mediaItemID.Bytes).String(),
|
||||
MediaItemTitle: title,
|
||||
ConflictType: conflictType,
|
||||
ConflictData: conflictData,
|
||||
ResolutionStatus: resolutionStatus,
|
||||
CreatedAt: createdAt.Time,
|
||||
}
|
||||
if resolvedBy.Valid {
|
||||
detail.ResolvedBy = uuid.UUID(resolvedBy.Bytes).String()
|
||||
}
|
||||
if resolvedAt.Valid {
|
||||
detail.ResolvedAt = &resolvedAt.Time
|
||||
}
|
||||
if resolutionData != nil {
|
||||
if err := json.Unmarshal(resolutionData, &detail.ResolutionData); err != nil {
|
||||
log.Printf("conflict %s: unparsable resolution data: %v", detail.ID, err)
|
||||
}
|
||||
}
|
||||
return detail
|
||||
}
|
||||
|
||||
func (h *ConflictHandler) ListConflicts(c *echo.Context) error {
|
||||
conflicts, total, unresolved, err := h.GetConflictsData(c)
|
||||
if err != nil {
|
||||
@@ -177,26 +197,10 @@ func (h *ConflictHandler) GetConflict(c *echo.Context) error {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "failed to parse conflict data")
|
||||
}
|
||||
|
||||
detail := ConflictDetailResponse{
|
||||
ID: uuid.UUID(conflict.ID.Bytes).String(),
|
||||
MediaItemID: uuid.UUID(conflict.MediaItemID.Bytes).String(),
|
||||
MediaItemTitle: mediaItem.Title,
|
||||
ConflictType: conflict.ConflictType,
|
||||
ConflictData: conflictData,
|
||||
ResolutionStatus: conflict.ResolutionStatus.String,
|
||||
CreatedAt: conflict.CreatedAt.Time,
|
||||
}
|
||||
|
||||
if conflict.ResolvedBy.Valid {
|
||||
detail.ResolvedBy = uuid.UUID(conflict.ResolvedBy.Bytes).String()
|
||||
}
|
||||
if conflict.ResolvedAt.Valid {
|
||||
detail.ResolvedAt = &conflict.ResolvedAt.Time
|
||||
}
|
||||
if conflict.ResolutionData != nil {
|
||||
if err := json.Unmarshal(conflict.ResolutionData, &detail.ResolutionData); err == nil {
|
||||
}
|
||||
}
|
||||
detail := NewConflictDetail(conflict.ID, conflict.MediaItemID,
|
||||
mediaItem.Title, conflict.ConflictType, conflict.ResolutionStatus.String,
|
||||
conflictData, conflict.CreatedAt, conflict.ResolvedAt, conflict.ResolvedBy,
|
||||
conflict.ResolutionData)
|
||||
|
||||
return c.JSON(http.StatusOK, detail)
|
||||
}
|
||||
|
||||
@@ -217,6 +217,17 @@ func (h *DeviceHandler) CheckRegistrationStatus(c *echo.Context) error {
|
||||
})
|
||||
}
|
||||
|
||||
// koreaderSyncEndpoints is the KOReader endpoint map shared by the
|
||||
// surfaces that publish it (KOReader authenticates by header, so its
|
||||
// URLs never embed the token).
|
||||
func koreaderSyncEndpoints(baseURL string) map[string]string {
|
||||
return map[string]string{
|
||||
"progress": fmt.Sprintf("%s/api/sync/koreader/progress", baseURL),
|
||||
"metadata": fmt.Sprintf("%s/api/sync/koreader/metadata", baseURL),
|
||||
"bookmarks": fmt.Sprintf("%s/api/sync/koreader/bookmarks", baseURL),
|
||||
}
|
||||
}
|
||||
|
||||
// deviceToInfo is the single Devices->DeviceInfo mapper for every
|
||||
// device surface. AuthToken rides only on surfaces that deliberately
|
||||
// expose it (the owner's device lists).
|
||||
@@ -483,9 +494,7 @@ func (h *DeviceHandler) RegenerateDeviceToken(c *echo.Context) error {
|
||||
syncURLs["bookmark"] = fmt.Sprintf("%s/api/sync/kobo/%s/bookmark", baseURL, newToken)
|
||||
syncURLs["init"] = fmt.Sprintf("%s/api/sync/kobo/%s/v1/initialization", baseURL, newToken)
|
||||
case "koreader":
|
||||
syncURLs["progress"] = fmt.Sprintf("%s/api/sync/koreader/progress", baseURL)
|
||||
syncURLs["metadata"] = fmt.Sprintf("%s/api/sync/koreader/metadata", baseURL)
|
||||
syncURLs["bookmarks"] = fmt.Sprintf("%s/api/sync/koreader/bookmarks", baseURL)
|
||||
syncURLs = koreaderSyncEndpoints(baseURL)
|
||||
}
|
||||
|
||||
// Handle HTMX requests - return HTML with page reload script
|
||||
@@ -626,9 +635,7 @@ func (h *DeviceHandler) ApproveDevice(c *echo.Context) error {
|
||||
syncEndpoints := map[string]string{}
|
||||
switch registration.DeviceType {
|
||||
case "koreader":
|
||||
syncEndpoints["progress"] = fmt.Sprintf("%s/api/sync/koreader/progress", h.cfg.BaseURL)
|
||||
syncEndpoints["metadata"] = fmt.Sprintf("%s/api/sync/koreader/metadata", h.cfg.BaseURL)
|
||||
syncEndpoints["bookmarks"] = fmt.Sprintf("%s/api/sync/koreader/bookmarks", h.cfg.BaseURL)
|
||||
syncEndpoints = koreaderSyncEndpoints(h.cfg.BaseURL)
|
||||
case "kobo":
|
||||
syncEndpoints["markup"] = fmt.Sprintf("%s/api/sync/kobo/markup", h.cfg.BaseURL)
|
||||
syncEndpoints["library"] = fmt.Sprintf("%s/api/sync/kobo/library", h.cfg.BaseURL)
|
||||
|
||||
@@ -1590,15 +1590,11 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
if conf.ResolutionStatus.Valid && conf.ResolutionStatus.String == "unresolved" {
|
||||
var conflictData map[string]handlers.ConflictSourceData
|
||||
if err := json.Unmarshal(conf.ConflictData, &conflictData); err == nil {
|
||||
activeConflict = &handlers.ConflictDetailResponse{
|
||||
ID: uuid.UUID(conf.ID.Bytes).String(),
|
||||
MediaItemID: uuid.UUID(conf.MediaItemID.Bytes).String(),
|
||||
MediaItemTitle: mediaItem.Title,
|
||||
ConflictType: conf.ConflictType,
|
||||
ConflictData: conflictData,
|
||||
ResolutionStatus: conf.ResolutionStatus.String,
|
||||
CreatedAt: conf.CreatedAt.Time,
|
||||
}
|
||||
detail := handlers.NewConflictDetail(conf.ID, conf.MediaItemID,
|
||||
mediaItem.Title, conf.ConflictType, conf.ResolutionStatus.String,
|
||||
conflictData, conf.CreatedAt, conf.ResolvedAt, conf.ResolvedBy,
|
||||
conf.ResolutionData)
|
||||
activeConflict = &detail
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user