From 082a3e0c3c1ce9dde62c83b3c052824d461abbe6 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sat, 17 Oct 2026 16:38:00 -0400 Subject: [PATCH] fix(sync): offline detector wiped device name, auto_sync, and metadata on every transition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setOfflineMode/setOnlineMode only mean to flip sync_enabled, but they called the full-row UpdateDevice with Go zero values — which wrote DeviceName: "" and NULLed auto_sync/sync_frequency_minutes/ device_metadata on the device row at every offline->online flip. New targeted SetDeviceSyncEnabled query touches exactly that column; the real settings update keeps using UpdateDevice. Caught as part of the H4 helper-census pass (plan §5.4 erasure list). --- internal/database/querier.go | 1 + internal/database/queries.sql.go | 36 +++++++++++++++++++++++++++ internal/database/queries/queries.sql | 8 ++++++ internal/sync/offline.go | 14 ++++++----- 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/internal/database/querier.go b/internal/database/querier.go index fe9f03b6..71ef3660 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -526,6 +526,7 @@ type Querier interface { SearchNarratorValues(ctx context.Context, arg SearchNarratorValuesParams) ([]SearchNarratorValuesRow, error) SearchSeriesValues(ctx context.Context, arg SearchSeriesValuesParams) ([]SearchSeriesValuesRow, error) SearchTagsValues(ctx context.Context, arg SearchTagsValuesParams) ([]SearchTagsValuesRow, error) + SetDeviceSyncEnabled(ctx context.Context, arg SetDeviceSyncEnabledParams) (Devices, error) // Library Visibility queries SetLibraryVisibility(ctx context.Context, arg SetLibraryVisibilityParams) (LibraryVisibility, error) // Set system config diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index 45e25879..1010a052 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -13116,6 +13116,42 @@ func (q *Queries) SearchTagsValues(ctx context.Context, arg SearchTagsValuesPara return items, nil } +const SetDeviceSyncEnabled = `-- name: SetDeviceSyncEnabled :one +UPDATE devices +SET + sync_enabled = $2, + updated_at = NOW() +WHERE id = $1 +RETURNING id, user_id, device_name, device_type, device_identifier, auth_token, last_sync, last_seen, sync_enabled, auto_sync, sync_frequency_minutes, device_metadata, created_at, updated_at +` + +type SetDeviceSyncEnabledParams struct { + ID pgtype.UUID `db:"id" json:"id"` + SyncEnabled pgtype.Bool `db:"sync_enabled" json:"sync_enabled"` +} + +func (q *Queries) SetDeviceSyncEnabled(ctx context.Context, arg SetDeviceSyncEnabledParams) (Devices, error) { + row := q.db.QueryRow(ctx, SetDeviceSyncEnabled, arg.ID, arg.SyncEnabled) + var i Devices + err := row.Scan( + &i.ID, + &i.UserID, + &i.DeviceName, + &i.DeviceType, + &i.DeviceIdentifier, + &i.AuthToken, + &i.LastSync, + &i.LastSeen, + &i.SyncEnabled, + &i.AutoSync, + &i.SyncFrequencyMinutes, + &i.DeviceMetadata, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const SetLibraryVisibility = `-- name: SetLibraryVisibility :one INSERT INTO library_visibility (user_id, library_id, is_visible) VALUES ($1, $2, $3) diff --git a/internal/database/queries/queries.sql b/internal/database/queries/queries.sql index 497afe2f..ff4bc7dc 100644 --- a/internal/database/queries/queries.sql +++ b/internal/database/queries/queries.sql @@ -1669,6 +1669,14 @@ SET WHERE id = $1 RETURNING *; +-- name: SetDeviceSyncEnabled :one +UPDATE devices +SET + sync_enabled = $2, + updated_at = NOW() +WHERE id = $1 +RETURNING *; + -- name: UpdateDeviceLastSeen :one UPDATE devices SET diff --git a/internal/sync/offline.go b/internal/sync/offline.go index ef3881e6..c8222800 100644 --- a/internal/sync/offline.go +++ b/internal/sync/offline.go @@ -133,11 +133,12 @@ func (d *OfflineDetector) handleReconnectedDevice(ctx context.Context, status De } func (d *OfflineDetector) setOfflineMode(ctx context.Context, deviceID pgtype.UUID) { - _, err := d.db.UpdateDevice(ctx, database.UpdateDeviceParams{ + // Targeted flip: the full-row UpdateDevice used here zeroed the + // device name, auto_sync, frequency, and metadata on every + // transition (pg zero values are not "leave alone"). + _, err := d.db.SetDeviceSyncEnabled(ctx, database.SetDeviceSyncEnabledParams{ ID: deviceID, - DeviceName: "", SyncEnabled: pgtype.Bool{Bool: false, Valid: true}, - AutoSync: pgtype.Bool{}, }) if err != nil { @@ -146,11 +147,12 @@ func (d *OfflineDetector) setOfflineMode(ctx context.Context, deviceID pgtype.UU } func (d *OfflineDetector) setOnlineMode(ctx context.Context, deviceID pgtype.UUID) { - _, err := d.db.UpdateDevice(ctx, database.UpdateDeviceParams{ + // Targeted flip: the full-row UpdateDevice used here zeroed the + // device name, auto_sync, frequency, and metadata on every + // transition (pg zero values are not "leave alone"). + _, err := d.db.SetDeviceSyncEnabled(ctx, database.SetDeviceSyncEnabledParams{ ID: deviceID, - DeviceName: "", SyncEnabled: pgtype.Bool{Bool: true, Valid: true}, - AutoSync: pgtype.Bool{}, }) if err != nil {