From b3a359760c09dfd59d5b806b86e7c75c2175f140 Mon Sep 17 00:00:00 2001 From: timmy Date: Tue, 18 Aug 2026 18:39:01 +0000 Subject: [PATCH 1/2] feat: share private Today summaries (Closes #1090) --- .gitea/workflows/ci.yml | 2 +- README.md | 2 + frontend/dashboard.css | 15 ++ frontend/dashboard.js | 7 +- frontend/index.html | 24 ++ frontend/service-worker.js | 1 + frontend/today-recap.js | 6 +- frontend/today-summary.js | 223 ++++++++++++++++++ frontend/today-wrap-up.js | 11 +- src/frontend_bundle.py | 2 +- .../e2e/test_mobile_today_summary_release.py | 111 +++++++++ tests/test_ci_workflow.py | 1 + tests/test_service_worker.py | 1 + tests/test_today_recap.py | 4 +- tests/test_today_summary.py | 155 ++++++++++++ tests/test_today_wrap_up.py | 2 +- 16 files changed, 556 insertions(+), 11 deletions(-) create mode 100644 frontend/today-summary.js create mode 100644 tests/e2e/test_mobile_today_summary_release.py create mode 100644 tests/test_today_summary.py diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 4632a16..b9b5012 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -57,7 +57,7 @@ jobs: pip install -r requirements-e2e.txt python3 -m playwright install --with-deps chromium - name: Exercise packaged mobile work journeys - run: python3 -m pytest tests/e2e/test_mobile_offline_issue_release.py tests/e2e/test_mobile_search_preview_navigation.py tests/e2e/test_mobile_find_work_release.py tests/e2e/test_mobile_home_bootstrap_release.py tests/e2e/test_mobile_today_handoff_release.py tests/e2e/test_mobile_today_wrap_up_release.py tests/e2e/test_mobile_wrap_up_handoff_release.py -q + run: python3 -m pytest tests/e2e/test_mobile_offline_issue_release.py tests/e2e/test_mobile_search_preview_navigation.py tests/e2e/test_mobile_find_work_release.py tests/e2e/test_mobile_home_bootstrap_release.py tests/e2e/test_mobile_today_handoff_release.py tests/e2e/test_mobile_today_wrap_up_release.py tests/e2e/test_mobile_today_summary_release.py tests/e2e/test_mobile_wrap_up_handoff_release.py -q release-candidate: runs-on: ubuntu-latest diff --git a/README.md b/README.md index eb0b7db..6638521 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,8 @@ as unknown rather than unblocked. Completing any Today item now exposes a 10-sec 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 wrap-up, **Share day summary** opens a private review of the exact worked-on and tomorrow selections. Every row is opt-in adjustable, actual time is excluded by default, and an optional bounded note is previewed before the native share sheet or clipboard fallback. Canceling or closing keeps the account-scoped device draft; successful sharing or **Discard draft** removes it. + A running issue or pull request also exposes **Add update** without advancing Today. The operator can type or dictate a progress note, review and explicitly append, replace, or discard the transcript, add up to five photo-evidence items, then save privately or admit the exact comment to durable delivery. When work is blocked, **Post blocker & move on** requires a future return time, admits the comment before any planning change, defers the item to Later, and continues the existing Today session. An admitted planning transition remains checkpointed for retry, so a storage or Today-removal failure cannot post the blocker twice. Final transcripts—not audio—are bounded to 2,000 characters and isolated by confirmed account and Today item; closing the sheet aborts listening while leaving text and photos usable. 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 b5bc5f6..5435ea8 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -342,6 +342,21 @@ textarea { resize: vertical; min-height: 120px; } .today-wrap-up-item input { width:24px; min-height:24px; } .today-wrap-up-actions { position:sticky; bottom:0; margin:16px -6px -6px; padding:12px 6px; padding-bottom:calc(12px + env(safe-area-inset-bottom)); background:rgba(11,21,38,.98); border-top:1px solid #2a496e; } .today-wrap-up-actions button { min-height:44px; width:100%; } +.today-summary-sheet { position:fixed; inset:0; z-index:90; display:flex; align-items:flex-end; justify-content:center; background:rgba(5,12,21,.82); backdrop-filter:blur(4px); } +.today-summary-sheet[hidden] { display:none; } +.today-summary-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 #2a496e; border-radius:18px 18px 0 0; background:#0b1526; } +.today-summary-header { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; } +.today-summary-header h2 { margin:.2rem 0; } +.today-summary-header button { min-height:44px; min-width:44px; } +.today-summary-items { display:grid; gap:8px; } +.today-summary-item, .today-summary-option { display:flex; align-items:center; gap:10px; min-height:44px; padding:10px; border:1px solid #2a496e; border-radius:10px; background:#10233d; overflow-wrap:anywhere; } +.today-summary-item input, .today-summary-option input { width:24px; min-height:24px; flex:0 0 auto; } +.today-summary-item span { display:block; min-width:0; } +.today-summary-note { display:grid; gap:6px; margin:14px 0; font-weight:700; } +.today-summary-note textarea { box-sizing:border-box; width:100%; min-height:88px; resize:vertical; } +.today-summary-preview { min-height:72px; white-space:pre-wrap; overflow-wrap:anywhere; padding:12px; border:1px solid #31577f; border-radius:10px; background:#07101e; color:#e8f1ff; } +.today-summary-actions { position:sticky; bottom:0; display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:16px -6px -6px; padding:12px 6px; padding-bottom:calc(12px + env(safe-area-inset-bottom)); background:rgba(11,21,38,.98); border-top:1px solid #2a496e; } +.today-summary-actions button { min-height:44px; width:100%; } .today-handoff-dialog { box-sizing:border-box; width:min(620px,100%); max-width:none; max-height:100dvh; margin:auto auto 0; padding:0; color:#e8f1ff; border:1px solid #2a496e; border-radius:18px 18px 0 0; background:#0b1526; } .today-handoff-dialog::backdrop { background:rgba(5,12,21,.82); backdrop-filter:blur(4px); } .today-handoff-panel { max-height:100dvh; overflow:auto; overflow-x:hidden; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); } diff --git a/frontend/dashboard.js b/frontend/dashboard.js index 6da5ff8..22619ac 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -1902,18 +1902,21 @@ interruptionPrompt.resolve(button.dataset.todayInterruption) ); }); + const todaySummaryView = setupTodaySummary({ qs, escapeHtml, getLogin:() => planningOwnerLogin }); + todaySummaryView.resume(); const todayWrapUpView = setupTodayWrapUp({ todayWork, laterWork, todaySync, qs, escapeHtml, - onComplete:() => { + onComplete:(_result, _actualMinutes, workedItems, tomorrowItems) => { todayRecapView['completeReplan'](); refreshMyWorkView(); warmTodayOffline(); + todaySummaryView.open(workedItems, tomorrowItems); }, }); const todayRecapView = setupTodayRecap( timer, timerView, todayWork, api, qs, escapeHtml, closeOpenWorkSheets, updateWorkSessionActions, () => planningOwnerLogin, identity => [...todayMyWork, ...activeMyWork].find(item => todayWork.identity(item) === identity) || null, - actualMinutes => todayWrapUpView.open(todayMyWork, actualMinutes) + (actualMinutes, workedItems) => todayWrapUpView.open(todayMyWork, actualMinutes, workedItems) ); function updateDetailDeferLabels(active) { document.querySelectorAll('[data-detail-defer-preset=today]').forEach(button => { diff --git a/frontend/index.html b/frontend/index.html index 564037a..fa3c9ae 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -507,6 +507,29 @@ + +