Backfill data missed by devices with a pre-tag-sync token

/api/sync/ only returns rows changed since a device's last sync token.
Any device that synced before tag/time-entry syncing existed already
has a token from that era; pre-existing tags untouched since then were
never "changed since" that token and so were permanently invisible to
that device, even after tag syncing shipped. Adds a sync_format_version
check that ignores the stored token and forces exactly one full resync
per device when it detects an old-format token, backfilling anything
that was missed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Keith Smith
2026-09-05 00:15:14 -06:00
co-authored by Claude Sonnet 5
parent 3fbab2b831
commit 373bfe6a6e
2 changed files with 18 additions and 3 deletions
+17 -2
View File
@@ -7,6 +7,15 @@
const BACKGROUND_SYNC_MIN_INTERVAL_MS = 2 * 60 * 1000;
// Bump this whenever a previously-untracked entity type starts being synced
// (e.g. tags/time_entries were added after tasks-only syncing already
// shipped). /api/sync/ only returns rows changed since a device's last sync
// token, so any device with an old token would otherwise never receive
// pre-existing rows of the newly-tracked type - they were never "changed
// since" a token issued before that type existed at all. Forces exactly one
// full resync (by clearing the stored token) per bump, per device.
const SYNC_FORMAT_VERSION = 2;
function getCsrfToken() {
const el = document.querySelector('[name=csrfmiddlewaretoken]');
return el ? el.value : null;
@@ -103,7 +112,9 @@ async function runBackgroundSync() {
}
const deviceId = await ensureDeviceId();
const lastSyncToken = await getMeta('last_sync_token');
const syncFormatVersion = await getMeta('sync_format_version');
const forceFullResync = (syncFormatVersion || 1) < SYNC_FORMAT_VERSION;
const lastSyncToken = forceFullResync ? null : await getMeta('last_sync_token');
const dirtyTasks = await getDirtyTasks();
const dirtyTags = await getDirtyTags();
@@ -154,8 +165,12 @@ async function runBackgroundSync() {
await setMeta('last_sync_token', data.sync_token);
await setMeta('last_sync_at', new Date().toISOString());
await setMeta('sync_format_version', SYNC_FORMAT_VERSION);
return hadPendingChanges;
// A forced full resync just backfilled previously-missed data (e.g. tags
// that existed before tag syncing shipped) - worth a refresh even if
// nothing local was dirty, since the current page may be missing it too.
return hadPendingChanges || forceFullResync;
}
async function maybeBackgroundSync() {