From f64ce3acc43fb23ec63af12082b3005925dc28e6 Mon Sep 17 00:00:00 2001
From: timmy
Date: Fri, 21 Aug 2026 14:37:50 +0000
Subject: [PATCH] feat: start Week Ahead days early (Closes #1224)
---
README.md | 7 +++-
frontend/dashboard.css | 2 +
frontend/dashboard.js | 4 +-
frontend/week-plan.js | 43 ++++++++++++++++++--
src/main.py | 27 +++++++++++-
src/today_store.py | 4 +-
tests/test_week_plan.py | 66 ++++++++++++++++++++++++++++++
tests/test_week_plan_frontend.py | 70 ++++++++++++++++++++++++++++++++
8 files changed, 212 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index cd3fba3..e1eb338 100644
--- a/README.md
+++ b/README.md
@@ -168,8 +168,11 @@ foreground, and midnight lifecycle checks share one delivery flight. A successfu
the pending copy, while a revision conflict preserves both the phone plan and fresh server snapshot for
review. Unsynced Tomorrow work is never promoted into Today.
**Week Ahead** opens as a read-first seven-day mobile overview, so operators can inspect the next planned day,
-work titles and references, load versus capacity, overloads, and pending sync without staging a change. **Edit day**
-enters one date and returns to the refreshed overview; **Edit week** starts the continuous planning pass.
+work titles and references, load versus capacity, overloads, and pending sync without staging a change. When Today
+is empty and no work session is active, **Start this day early** confirms the next planned date, item count, minutes,
+and capacity before atomically moving only that day into Today; offline, pending, stale, or non-empty plans remain
+unchanged. **Edit day** enters one date and returns to the refreshed overview; **Edit week** starts the continuous
+planning pass.
**Plan Week Ahead** continues through seven local dates and now finishes on a mobile review step instead of
closing after the seventh save. The review shows planned minutes against each day’s capacity, marks overloads,
and flags work assigned to more than one date. Operators can move an item to another date without copying it;
diff --git a/frontend/dashboard.css b/frontend/dashboard.css
index 85f986a..abe421d 100644
--- a/frontend/dashboard.css
+++ b/frontend/dashboard.css
@@ -333,6 +333,8 @@ textarea { resize: vertical; min-height: 120px; }
.week-review-item-open { display:block; width:100%; min-width:0; min-height:44px; padding:8px; border:0; border-radius:8px; background:transparent; color:inherit; text-align:left; }
.week-review-item-open:hover { background:#173453; }
.week-review-item-open:focus-visible { outline:3px solid #93c5fd; outline-offset:2px; }
+.week-start-early { display:block; width:100%; min-height:44px; margin-top:12px; border-color:#60a5fa; background:#1d4f7a; color:#eff6ff; font-weight:800; }
+.week-start-early:focus-visible { outline:3px solid #bfdbfe; outline-offset:2px; }
.week-review-item-copy strong, .week-review-item-copy small { overflow-wrap:anywhere; }
.week-review-item-copy small { color:#a9bdd3; }
.week-review-move { display:flex; align-items:end; gap:8px; }
diff --git a/frontend/dashboard.js b/frontend/dashboard.js
index 594f676..25f4fe0 100644
--- a/frontend/dashboard.js
+++ b/frontend/dashboard.js
@@ -457,7 +457,7 @@
getItem:weekItem,openItem:openRoutedWork,
openPlanner:openPlanToday,setReviewMode:value=>qs('#plan-today-sheet').classList.toggle('week-review-mode',value),
escapeHtml,escapeAttribute:escAttr,todayWork,
- refresh:refreshMyWorkView,warm:warmTodayOffline});
+ t:()=>latestTodayPlan,r:refreshMyWorkView,w:warmTodayOffline});
const weekCalendar=StackchainWeekCalendar.mountWeekCalendarHandoff({qs,getItem:weekItem,escapeHtml,escapeAttribute:escAttr,
onDone:()=>{weekFlow.finish();taskOverlayHistory.leave();},
});
@@ -7881,7 +7881,7 @@
try {
await weekPlan.keepLocal();
qs('#mobile-week-summary').textContent=weekPlan.summary();
- qs('#my-work-action-status').textContent='This phone’s Week Ahead plan is saved to your account. Today was not changed.';
+ qs('#my-work-action-status').textContent='This phone’s Week Ahead plan is saved. Today was not changed.';
closePlanToday();
} catch(error) {
const conflict=weekPlan.conflict();
diff --git a/frontend/week-plan.js b/frontend/week-plan.js
index 40b89e2..2d4bec3 100644
--- a/frontend/week-plan.js
+++ b/frontend/week-plan.js
@@ -406,6 +406,17 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
plan_date:due.plan_date,today_revision:todayRevision,
})});
}
+ async function startEarly(planDate,todayRevision) {
+ if(offlineSnapshot||pending()||conflict()||!Number.isInteger(todayRevision))return false;
+ const day=week.days.find(item=>item.plan_date===planDate&&item.ids.length);
+ if(!day)return false;
+ const promoted=await fetchJson('api/v1/week/start-early',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
+ promotion_id:`early-${day.plan_date}-r${week.revision}`,week_revision:week.revision,
+ plan_date:day.plan_date,today_revision:todayRevision,
+ })});
+ await load();
+ return promoted;
+ }
async function reconcilePromotion(preserved,selection) {
if(offlineSnapshot||pending()||conflict()) return false;
const due=(preserved?.week?.days||week.days).find(item=>item.plan_date<=localDate()&&item.ids.length);
@@ -425,12 +436,13 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
return label+(pending()?' · sync pending':'');
}
return {adopt,state,dates,day,pass,load,saveDay,stageDay,stageCapacities,review,move,placement,place,pending,flush,conflict,chooseDay,saveMerged,
- keepLocal,useRemote,promote,reconcile:reconcilePromotion,summary,rememberItems,rememberPendingItem,
+ keepLocal,useRemote,promote,startEarly,reconcile:reconcilePromotion,summary,rememberItems,rememberPendingItem,
item:id=>pendingItems[id]||confirmedItems[id]||null,
offline:()=>offlineSnapshot};
}
function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPlanner,setReviewMode=()=>{},escapeHtml,escapeAttribute,
- todayWork,refresh,warm}={}) {
+ todayWork,refresh:r=()=>{},warm:w=()=>{},today:t=()=>null,r:refresh=r,w:warm=w,t:getTodayPlan=t,
+ confirmEarly=message=>globalThis.confirm?.(message)??false}={}) {
let selectedDate=null;
let reviewing=false;
let overviewing=false;
@@ -521,7 +533,10 @@ function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPla
function renderReview() {
const value=controller.review(),root=qs('#week-review-days'),duplicates=qs('#week-review-duplicates');
const readOnly=Boolean(controller.offline?.());
+ const pending=Boolean(controller.pending?.());
const nextUp=overviewing?value.days.find(day=>day.ids.length):null;
+ const today=getTodayPlan?.();
+ const canStartEarly=Boolean(nextUp&&!readOnly&&!pending&&Number.isInteger(today?.revision)&&!today.ids?.length);
const destinations=value.days.map(day=>'').join('');
const itemMarkup=(id,day)=>{
const item=getItem(id)||controller.item?.(id),estimate=Number(day.estimates?.[id])||0;
@@ -540,14 +555,14 @@ function createWeekPlanWorkflow({controller,qs,getItem=()=>null,openItem,openPla
const items=day.ids.length?'
'+escapeHtml(day.label)+(day===nextUp?' Next up':'')+'
'+edit+'
'+
- escapeHtml(load)+'
'+items+'';
+ escapeHtml(load)+'
'+items+start+'';
}).join('');
duplicates.hidden=!value.duplicates.length;
duplicates.innerHTML=value.duplicates.length?'Choose one date for duplicated work before confirming.