From b81d695585c36f0df760e42cbee0b405a600f59f Mon Sep 17 00:00:00 2001 From: timmy Date: Wed, 12 Aug 2026 12:34:55 +0000 Subject: [PATCH] feat: undo accidental update acknowledgements (Closes #649) --- frontend/dashboard.css | 3 ++ frontend/dashboard.js | 23 ++++++++- frontend/index.html | 6 +++ frontend/my-work.js | 2 +- frontend/notification-undo.js | 85 +++++++++++++++++++++++++++++++++ frontend/service-worker.js | 1 + src/frontend_bundle.py | 2 +- src/gitea_proxy.py | 8 ++++ src/main.py | 21 ++++++++ tests/test_my_work.py | 57 +++++++++++++++++++++- tests/test_notification_read.py | 43 +++++++++++++++++ tests/test_service_worker.py | 1 + 12 files changed, 247 insertions(+), 5 deletions(-) create mode 100644 frontend/notification-undo.js diff --git a/frontend/dashboard.css b/frontend/dashboard.css index 45679c1..0cfd1a4 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -10,6 +10,9 @@ header { position: sticky; top: 0; z-index: 20; padding: 12px 16px; display:flex .app-menu > summary { display:none; } .app-menu-panel { display:flex; gap:10px; align-items:center; flex-wrap:wrap; } button { background: linear-gradient(180deg,#1f3a5f,#15324d); border:1px solid #2a496e; color:#e5e7eb; padding:8px 12px; border-radius:10px; cursor:pointer; } +.notification-undo { position:fixed; z-index:110; left:50%; bottom:calc(88px + env(safe-area-inset-bottom)); transform:translateX(-50%); box-sizing:border-box; width:min(520px,calc(100vw - 24px)); display:flex; align-items:center; justify-content:space-between; gap:12px; padding:10px 12px; border:1px solid #60a5fa; border-radius:12px; background:#10233d; box-shadow:0 12px 36px rgba(0,0,0,.5); overflow-wrap:anywhere; } +.notification-undo[hidden] { display:none; } +.notification-undo button { min-height:44px; min-width:64px; flex:none; } .draft-capacity-sheet { position:fixed; inset:0; z-index:96; display:flex; align-items:flex-end; justify-content:center; background:rgba(5,12,21,.82); backdrop-filter:blur(4px); } .draft-capacity-sheet[hidden] { display:none; } .draft-capacity-panel { box-sizing:border-box; width:min(620px,100%); max-height:100dvh; overflow:auto; overflow-x:hidden; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); border:1px solid #b45309; border-radius:18px 18px 0 0; background:#0b1526; } diff --git a/frontend/dashboard.js b/frontend/dashboard.js index 3163d24..69cd98c 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -786,6 +786,15 @@ if (state.message) qs('#update-sheet-status').textContent = state.message; }, }); + const notificationUndo = createDashboardNotificationUndo({ + restore: requestNotificationUnread, + getItems: () => lastMyWork, + setItems: items => { lastMyWork = items; }, + getNotifications: () => lastNotifications, + setNotifications: items => { lastNotifications = items; }, + refresh: refreshMyWorkView, + select: qs, + }); const notificationReader = createNotificationReader({ load: fetchNotificationDetail, loadConversation: fetchNotificationConversation, @@ -5034,6 +5043,9 @@ if (result?.accepted) qs('#my-work-action-status').textContent = result.delivery === 'posted' ? 'Reply posted and update marked read.' : 'Reply and read acknowledgement queued for sync.'; + if (result?.delivery === 'posted' && result?.next) { + notificationUndo.offer(item, result.next.items); + } } catch (error) { qs('#update-reply-status').textContent = error.message + ' Your draft is safe; retry.'; qs('#update-reply').focus(); @@ -5046,7 +5058,8 @@ qs('#mark-update-read-next').addEventListener('click', async () => { qs('#mark-update-read-next').disabled = true; try { - await notificationReader.markReadAndNext(lastMyWork); + const result = await notificationReader.markReadAndNext(lastMyWork); + if (result) notificationUndo.offer(result.item, result.items); } finally { qs('#mark-update-read-next').disabled = false; } @@ -5056,11 +5069,17 @@ button.disabled = true; try { const result = await notificationReader.acknowledgeAndNext(lastMyWork); - if (result) qs('#my-work-action-status').textContent = 'Update acknowledged with 👍.'; + if (result) notificationUndo.offer(result.item, result.items); } finally { button.disabled = offlineWorkMode; } }); + qs('#undo-notification').addEventListener('click', async () => { + const button = qs('#undo-notification'); + button.disabled = true; + const restored = await notificationUndo.run(); + button.disabled = restored; + }); qs('#close-review-sheet').addEventListener('click', closeReviewSheet); qs('#retry-review-load').addEventListener('click', () => { if (selectedReview) openReviewSheet(selectedReview, reviewTrigger); diff --git a/frontend/index.html b/frontend/index.html index ef3aa98..ffc6ab3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -688,6 +688,11 @@ + +