From 373bfe6a6e707a371aa122754e2db286d24a9da5 Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Sat, 5 Sep 2026 00:15:14 -0600 Subject: [PATCH] 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 --- static/js/offline-sync.js | 19 +++++++++++++++++-- templates/base.html | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/static/js/offline-sync.js b/static/js/offline-sync.js index e529131..056641c 100644 --- a/static/js/offline-sync.js +++ b/static/js/offline-sync.js @@ -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() { diff --git a/templates/base.html b/templates/base.html index 51117ba..ad6fdb9 100644 --- a/templates/base.html +++ b/templates/base.html @@ -148,7 +148,7 @@ {% endblock %} - + {% block extra_js %}{% endblock %}