From b18aa5ed636c6413b7d127f8ab4d74adbd749cda Mon Sep 17 00:00:00 2001 From: timmy Date: Thu, 27 Aug 2026 09:27:27 +0000 Subject: [PATCH] feat: sync mobile queue priority across devices (Closes #1464) --- README.md | 8 + frontend/dashboard.css | 3 + frontend/dashboard.js | 12 +- frontend/index.html | 5 + frontend/mobile-queue-priority.js | 212 +++++++++++++++--- scripts/rotate_private_state.py | 3 + src/main.py | 54 ++++- src/queue_priority_store.py | 100 +++++++++ .../test_adaptive_mobile_queues_release.py | 2 +- tests/test_mobile_task_dock.py | 144 +++++++++++- tests/test_queue_priority_store.py | 75 +++++++ tests/test_rotate_private_state.py | 27 +++ 12 files changed, 609 insertions(+), 36 deletions(-) create mode 100644 src/queue_priority_store.py create mode 100644 tests/test_queue_priority_store.py diff --git a/README.md b/README.md index db86887..8ab89a8 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,14 @@ overwriting newer views. Rename and delete affect only the saved view, never Git sync service leaves ad-hoc Search usable. Set `STACKCHAIN_SAVED_SEARCH_DB` to override the default `.stackchain-state/saved-searches.sqlite3` path. +The mobile **Customize routine order** control is also portable across authenticated devices. Reordering +remains immediate when offline and is marked **Sync pending** until connectivity returns. The complete +routine order is stored as an encrypted, revisioned collection scoped to the confirmed Gitea login; a +concurrent edit shows explicit **Keep this device** and **Use other device** actions instead of silently +losing either order. Resetting publishes the canonical default order. Delivery, Human Gates, and active +Prepare Today precedence are not customizable. Set `STACKCHAIN_QUEUE_PRIORITY_DB` to override the +default `.stackchain-state/queue-priority.sqlite3` path. + Confirmed **Watch issue** and **Watch pull request** actions on open Search results and assigned My Work issue/pull-request details feed the mobile **Following** queue, including work already assigned to you or a teammate. The detail control loads authoritative Gitea state, remains single-flight while changing it, and refreshes Following only diff --git a/frontend/dashboard.css b/frontend/dashboard.css index 90ca0c9..ce0d300 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -1525,6 +1525,9 @@ textarea { resize: vertical; min-height: 120px; } .mobile-queue-priority-controls button { min-height:44px; min-width:64px; padding:6px 8px; } .mobile-queue-priority-footer { display:grid; gap:6px; padding:10px 12px 12px; } .mobile-queue-priority-footer button { min-height:44px; } + .mobile-queue-priority-conflict { margin:0 8px 10px; padding:10px; border:1px solid #f59e0b; border-radius:10px; background:#3b2808; } + .mobile-queue-priority-conflict p { margin:0 0 8px; } + .mobile-queue-priority-conflict button { min-height:44px; margin:4px 4px 0 0; } .mobile-queue-group { margin-top:16px; } .mobile-queue-group h3 { font-size:1rem; } .mobile-queue-all { margin-top:16px; } diff --git a/frontend/dashboard.js b/frontend/dashboard.js index 219844b..86b5e6f 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -433,17 +433,25 @@ mobileQueuePriority = createMobileQueuePriority({ storage: localStorage, getLogin: () => confirmedOwnerLogin, + fetchJson: fetchReviewJson, document, list: qs('#mobile-queue-priority-list'), resetButton: qs('#reset-mobile-queue-priority'), status: qs('#mobile-queue-priority-status'), + conflict: qs('#mobile-queue-priority-conflict'), + keepLocalButton: qs('#keep-local-mobile-queue-priority'), + useRemoteButton: qs('#use-remote-mobile-queue-priority'), labels: { attention:'Attention', today:'Today', update:'Updates', agenda:'Agenda', following:'Following', authored:'My PRs', filed:'Filed', later:'Later', draft:'Drafts', }, - onChange: () => renderMobileQueuePresentation(), + onChange: () => { + renderMobileQueuePresentation(); + void mobileQueuePriority.sync(); + }, }); mobileQueuePriority.start(); + window.addEventListener('online', () => { void mobileQueuePriority.sync(); }); let rR = null; function rRC() { if (rR) return rR; @@ -676,6 +684,7 @@ if (!response.ok) { const error = new Error(payload.error || payload.detail?.message || payload.detail || 'Review request failed.'); error.status = response.status; + error.payload = payload; error.code = payload.detail?.code; const retryAfter = response.headers.get('Retry-After'); @@ -5638,6 +5647,7 @@ confirmedOwnerLogin = activeFlushLogin; mobileQueuePriority.render(); renderMobileQueuePresentation(); + void mobileQueuePriority.load(); void refreshPhotoDraftInbox(); timerView.restore(todaySync.flush()); restoreReleaseReceipt(); diff --git a/frontend/index.html b/frontend/index.html index 5b64cb2..fd3396c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2191,6 +2191,11 @@ +