From 020106f0017855f98bf67792d7c249e3f574ae9b Mon Sep 17 00:00:00 2001 From: timmy Date: Sun, 9 Aug 2026 00:39:08 +0000 Subject: [PATCH] feat: sync Today plan across devices (#357) --- frontend/dashboard.js | 46 ++++++- frontend/index.html | 2 + frontend/service-worker.js | 3 +- frontend/today-sync.js | 106 ++++++++++++++++ frontend/today-work.js | 12 +- src/main.py | 71 ++++++++++- src/today_store.py | 130 ++++++++++++++++++++ tests/test_markdown_renderer.py | 2 +- tests/test_mobile_composer_integration.py | 2 +- tests/test_service_worker.py | 7 +- tests/test_today_store.py | 105 ++++++++++++++++ tests/test_today_sync.py | 142 ++++++++++++++++++++++ tests/test_today_work.py | 25 +++- 13 files changed, 640 insertions(+), 13 deletions(-) create mode 100644 frontend/today-sync.js create mode 100644 src/today_store.py create mode 100644 tests/test_today_store.py create mode 100644 tests/test_today_sync.py diff --git a/frontend/dashboard.js b/frontend/dashboard.js index 14fac6f..e86a1e9 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -139,6 +139,23 @@ storage: localStorage, getLogin: () => planningOwnerLogin, }); + const todaySync = createTodaySync({ + storage: localStorage, + getLogin: () => planningOwnerLogin, + fetchJson: fetchReviewJson, + onRemoteIds: ids => { + if (!planningOwnerLogin || !todayWork.replace(ids)) return; + refreshMyWorkView(); + warmTodayOffline(); + }, + onStatus: state => { + const status = qs('#today-sync-status'); + status.textContent = state === 'saved' ? 'Today saved to account.' : + (state === 'pending' ? 'Today saved on this device · sync pending.' : + (state === 'full' ? 'Another device filled Today · showing its saved plan.' : + 'Today sync unavailable · changes stay on this device.')); + }, + }); const laterWork = createLaterWork({ storage: localStorage, getLogin: () => planningOwnerLogin, @@ -424,7 +441,11 @@ addToday: item => { const result = todayWork.add(item); refreshMyWorkView(); - if (result === 'added') warmTodayOffline(); + if (result === 'added') { + todaySync.enqueue('add', todayWork.identity(item)); + todaySync.flush(); + warmTodayOffline(); + } return result; }, onClaimed: item => { @@ -1077,19 +1098,32 @@ (result === 'added' ? 'Added to Today without changing Gitea.' : (result === 'exists' ? 'This item is already in Today.' : 'Could not save Today on this device.')); refreshMyWorkView(); - if (result === 'added') warmTodayOffline(); + if (result === 'added') { + const item = lastMyWork[Number(button.dataset.workIndex)]; + todaySync.enqueue('add', todayWork.identity(item)); + todaySync.flush(); + warmTodayOffline(); + } }); }); document.querySelectorAll('[data-today-remove]').forEach(button => { button.addEventListener('click', () => { - todayWork.remove(lastMyWork[Number(button.dataset.workIndex)]); + const item = lastMyWork[Number(button.dataset.workIndex)]; + if (todayWork.remove(item)) { + todaySync.enqueue('remove', todayWork.identity(item)); + todaySync.flush(); + } qs('#my-work-action-status').textContent = 'Removed from Today without changing Gitea.'; refreshMyWorkView(); }); }); document.querySelectorAll('[data-today-move]').forEach(button => { button.addEventListener('click', () => { - todayWork.move(lastMyWork[Number(button.dataset.workIndex)], button.dataset.todayMove); + const item = lastMyWork[Number(button.dataset.workIndex)]; + if (todayWork.move(item, button.dataset.todayMove)) { + todaySync.enqueue('move', todayWork.identity(item), button.dataset.todayMove); + todaySync.flush(); + } refreshMyWorkView(); document.querySelector('[data-today-move="' + button.dataset.todayMove + '"][data-work-index="' + button.dataset.workIndex + '"]')?.focus(); }); @@ -1971,6 +2005,10 @@ String(snapshot.context.user?.login || '').trim() : ''; planningOwnerLogin = retainedPlanningLogin; updatePlanningAvailability(); + if (planningOwnerLogin) { + todaySync.migrate(todayWork.read()); + todaySync.flush(); + } const contextIdentityFresh = !snapshot.context.error && !contextFreshness?.stale && !contextFreshness?.degraded && !contextFreshness?.revalidating; activeFlushLogin = contextIdentityFresh ? String(snapshot.context.user?.login || '').trim() : ''; diff --git a/frontend/index.html b/frontend/index.html index 01b7429..b9d43cc 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -99,6 +99,7 @@
+
Today is saved on this device.