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"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -103,26 +104,10 @@ func (h *ConflictHandler) GetConflictsData(c *echo.Context) ([]ConflictDetailRes
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
detail := ConflictDetailResponse{
|
detail := NewConflictDetail(conflict.ID, conflict.MediaItemID,
|
||||||
ID: uuid.UUID(conflict.ID.Bytes).String(),
|
conflict.Title, conflict.ConflictType, conflict.ResolutionStatus.String,
|
||||||
MediaItemID: uuid.UUID(conflict.MediaItemID.Bytes).String(),
|
conflictData, conflict.CreatedAt, conflict.ResolvedAt, conflict.ResolvedBy,
|
||||||
MediaItemTitle: conflict.Title,
|
conflict.ResolutionData)
|
||||||
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 {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response = append(response, detail)
|
response = append(response, detail)
|
||||||
if conflict.ResolutionStatus.String == "unresolved" {
|
if conflict.ResolutionStatus.String == "unresolved" {
|
||||||
@@ -133,6 +118,41 @@ func (h *ConflictHandler) GetConflictsData(c *echo.Context) ([]ConflictDetailRes
|
|||||||
return response, len(response), unresolvedCount, nil
|
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 {
|
func (h *ConflictHandler) ListConflicts(c *echo.Context) error {
|
||||||
conflicts, total, unresolved, err := h.GetConflictsData(c)
|
conflicts, total, unresolved, err := h.GetConflictsData(c)
|
||||||
if err != nil {
|
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")
|
return echo.NewHTTPError(http.StatusInternalServerError, "failed to parse conflict data")
|
||||||
}
|
}
|
||||||
|
|
||||||
detail := ConflictDetailResponse{
|
detail := NewConflictDetail(conflict.ID, conflict.MediaItemID,
|
||||||
ID: uuid.UUID(conflict.ID.Bytes).String(),
|
mediaItem.Title, conflict.ConflictType, conflict.ResolutionStatus.String,
|
||||||
MediaItemID: uuid.UUID(conflict.MediaItemID.Bytes).String(),
|
conflictData, conflict.CreatedAt, conflict.ResolvedAt, conflict.ResolvedBy,
|
||||||
MediaItemTitle: mediaItem.Title,
|
conflict.ResolutionData)
|
||||||
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 {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, detail)
|
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
|
// deviceToInfo is the single Devices->DeviceInfo mapper for every
|
||||||
// device surface. AuthToken rides only on surfaces that deliberately
|
// device surface. AuthToken rides only on surfaces that deliberately
|
||||||
// expose it (the owner's device lists).
|
// 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["bookmark"] = fmt.Sprintf("%s/api/sync/kobo/%s/bookmark", baseURL, newToken)
|
||||||
syncURLs["init"] = fmt.Sprintf("%s/api/sync/kobo/%s/v1/initialization", baseURL, newToken)
|
syncURLs["init"] = fmt.Sprintf("%s/api/sync/kobo/%s/v1/initialization", baseURL, newToken)
|
||||||
case "koreader":
|
case "koreader":
|
||||||
syncURLs["progress"] = fmt.Sprintf("%s/api/sync/koreader/progress", baseURL)
|
syncURLs = koreaderSyncEndpoints(baseURL)
|
||||||
syncURLs["metadata"] = fmt.Sprintf("%s/api/sync/koreader/metadata", baseURL)
|
|
||||||
syncURLs["bookmarks"] = fmt.Sprintf("%s/api/sync/koreader/bookmarks", baseURL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle HTMX requests - return HTML with page reload script
|
// 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{}
|
syncEndpoints := map[string]string{}
|
||||||
switch registration.DeviceType {
|
switch registration.DeviceType {
|
||||||
case "koreader":
|
case "koreader":
|
||||||
syncEndpoints["progress"] = fmt.Sprintf("%s/api/sync/koreader/progress", h.cfg.BaseURL)
|
syncEndpoints = koreaderSyncEndpoints(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)
|
|
||||||
case "kobo":
|
case "kobo":
|
||||||
syncEndpoints["markup"] = fmt.Sprintf("%s/api/sync/kobo/markup", h.cfg.BaseURL)
|
syncEndpoints["markup"] = fmt.Sprintf("%s/api/sync/kobo/markup", h.cfg.BaseURL)
|
||||||
syncEndpoints["library"] = fmt.Sprintf("%s/api/sync/kobo/library", 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" {
|
if conf.ResolutionStatus.Valid && conf.ResolutionStatus.String == "unresolved" {
|
||||||
var conflictData map[string]handlers.ConflictSourceData
|
var conflictData map[string]handlers.ConflictSourceData
|
||||||
if err := json.Unmarshal(conf.ConflictData, &conflictData); err == nil {
|
if err := json.Unmarshal(conf.ConflictData, &conflictData); err == nil {
|
||||||
activeConflict = &handlers.ConflictDetailResponse{
|
detail := handlers.NewConflictDetail(conf.ID, conf.MediaItemID,
|
||||||
ID: uuid.UUID(conf.ID.Bytes).String(),
|
mediaItem.Title, conf.ConflictType, conf.ResolutionStatus.String,
|
||||||
MediaItemID: uuid.UUID(conf.MediaItemID.Bytes).String(),
|
conflictData, conf.CreatedAt, conf.ResolvedAt, conf.ResolvedBy,
|
||||||
MediaItemTitle: mediaItem.Title,
|
conf.ResolutionData)
|
||||||
ConflictType: conf.ConflictType,
|
activeConflict = &detail
|
||||||
ConflictData: conflictData,
|
|
||||||
ResolutionStatus: conf.ResolutionStatus.String,
|
|
||||||
CreatedAt: conf.CreatedAt.Time,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user