From 6e677f885762133f83a423ad0255592442f0026a Mon Sep 17 00:00:00 2001 From: timmy Date: Mon, 17 Aug 2026 18:06:52 +0000 Subject: [PATCH] feat: undo accidental Today completion (Closes #1034) --- README.md | 5 +- frontend/dashboard.css | 9 +- frontend/index.html | 5 + frontend/service-worker.js | 2 +- frontend/today-completion.js | 68 +++++++++++- frontend/today-work.js | 25 ++++- .../e2e/test_mobile_today_handoff_release.py | 22 +++- tests/test_comment_next.py | 2 +- tests/test_later_sync.py | 2 +- tests/test_markdown_renderer.py | 2 +- tests/test_mobile_composer_integration.py | 2 +- tests/test_mobile_device_setup.py | 2 +- tests/test_mobile_insights.py | 2 +- tests/test_mobile_start_day.py | 2 +- tests/test_plan_today.py | 2 +- tests/test_service_worker.py | 28 ++--- tests/test_today_readiness.py | 2 +- tests/test_today_sync.py | 2 +- tests/test_today_work.py | 100 ++++++++++++++++++ 19 files changed, 250 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 8ca619c..d09853d 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,10 @@ time. An over-capacity plan requires a second explicit save, legacy plans migrat and an active Today session shows the current estimate plus estimated remaining runway. Adding an issue through **Plan Today** first previews its Gitea dependencies: unresolved blockers are listed with links and require the explicit **Add blocked item anyway** override, while an unavailable dependency lookup is reported -as unknown rather than unblocked. Starting a Today work session also +as unknown rather than unblocked. Completing any Today item now exposes a 10-second, touch-safe +**Undo** receipt. Undo restores the item's original order and estimate, queues the inverse cross-device +plan changes, and leaves the already-advanced work session on its current item; expiry, capacity, or a +concurrently changed plan is reported without overwriting newer work. Starting a Today work session also stores an account-bound checkpoint on the current device and starts an account-bound actual-time timer for the exact item. The sticky mobile session controls show elapsed time beside the estimate and let the operator pause or resume it. An opt-in, privacy-safe lock-screen notification mirrors the current pause/resume control and adds **Finish current**: its opaque one-shot action is bound to the exact active item, reuses **Done & next** or recap, and never changes the underlying Gitea issue or pull request. Switching items preserves each item's elapsed value, while wall-clock checkpoints keep a running timer accurate through app backgrounding, reloads, and installed-app restarts without double counting. **End session** stops accumulation but retains measured time with the private device data. The recap identifies each item by title and repository, reports per-item estimate variance, and **Save recap & adjust plan** continues into the current ordered Today plan without changing Gitea time entries. Eligible non-zero rows also offer an unchecked **Log Xm to Gitea** control. **Log selected time to Gitea** saves the recap and sends only those corrected durations to each canonical issue or pull request; confirmed account-scoped receipts prevent a completed row from being posted again, while definite failures retain the draft for an explicit retry. If the upstream response is lost after sending, Stackchain marks the row for verification in Gitea instead of risking an automatic duplicate. Actual time appears in planning as an explicit estimate recommendation; it changes only the planning draft until the operator chooses **Save plan** or **Save & start**. After the recap is confirmed, this recommendation handoff remains account-bound on the device through reloads, app restarts, planner cancellation, and failed plan admission. Opening **Plan Today** resumes it without reposting the recap; a successful plan save clears it, while **Discard recap feedback** removes only the handoff and leaves recap history unchanged. The recap and any corrected actual minutes are also saved as an account-bound device draft: an offline save failure can survive a reload and retry with the same idempotent session ID, while another account cannot view it. The draft and timer are cleared only after the account confirms the recap. After a reload or installed-app restart, **Resume Today** reopens the saved item (or the next surviving item if work changed). In an open diff --git a/frontend/dashboard.css b/frontend/dashboard.css index 00656fb..0b6de18 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -28,9 +28,11 @@ header { position: sticky; top: 0; z-index: 20; padding: 12px 16px; display:flex .app-menu-panel { display:flex; gap:10px; align-items:center; flex-wrap:wrap; } #open-insights { display:none; } 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; } +.notification-undo { position:fixed; } +.notification-undo button { min-height:44px; } +.notification-undo, .today-completion-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], .today-completion-undo[hidden] { display:none; } +.notification-undo button, .today-completion-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; } @@ -1071,6 +1073,7 @@ textarea { resize: vertical; min-height: 120px; } .mobile-today-hud [data-mobile-today-toggle] { grid-area:toggle; } .mobile-today-hud button { min-height:44px; max-width:100%; } .mobile-today-hud [data-work-session-adjust-plan] { grid-column:1 / -1; } + .today-completion-undo { bottom:calc(168px + env(safe-area-inset-bottom)); } } @media (min-width:701px) { .update-gesture-status { display:none; } } @media (prefers-reduced-motion: reduce) { diff --git a/frontend/index.html b/frontend/index.html index b317852..e2a8e52 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1308,6 +1308,11 @@ + +