feat: review and rebalance Week Ahead (Closes #1188)
CI / lint (pull_request) Successful in 3m31s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 4m48s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-20 17:36:02 +00:00
1 parent 69532bdc1f
commit d5f13c4afb
21 files changed
+351 -53

No files matched your search

@@ -76,6 +76,14 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
dates.nth(6).click()
expect(page.locator("#save-today-plan")).to_have_text("Save week")
page.locator("#save-today-plan").click()
expect(page.locator("#plan-today-sheet")).to_be_visible()
expect(page.locator("#week-review")).to_be_visible()
expect(page.locator("#week-review-days .week-review-day")).to_have_count(7)
page.wait_for_function("() => !document.querySelector('#confirm-week-plan').disabled")
confirm = page.locator("#confirm-week-plan")
bounds = confirm.bounding_box()
assert bounds and bounds["height"] >= 44
confirm.click()
expect(page.locator("#plan-today-sheet")).to_be_hidden()
page.wait_for_function("() => document.querySelector('#mobile-week-summary').textContent !== 'Loading Week Ahead…'")
assert saved and len(saved[-1]["days"]) == 2
+1 -1
View File
@@ -303,4 +303,4 @@ async def test_unread_update_offers_reply_mark_read_and_next_independent_of_toda
assert '.update-reply-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));' in html
assert '.update-reply-actions button { min-height:44px;' in html
worker = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v127" in worker
assert "stackchain-dashboard-shell-v128" in worker
+1 -1
View File
@@ -435,5 +435,5 @@ async def test_dashboard_syncs_every_later_change_and_exposes_account_status():
def test_later_sync_ships_atomically_in_the_offline_shell():
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v127" in source
assert "stackchain-dashboard-shell-v128" in source
assert "BASE + 'static/later-sync.js'" in source
+1 -1
View File
@@ -256,4 +256,4 @@ def test_markdown_work_bodies_are_mobile_safe_block_containers():
assert ".markdown-content { min-width:0; max-width:100%; overflow-wrap:anywhere;" in css
assert ".markdown-content pre { max-width:100%; overflow-x:auto;" in css
assert ".markdown-content a { min-height:44px;" in css
assert "stackchain-dashboard-shell-v127" in worker
assert "stackchain-dashboard-shell-v128" in worker
+1 -1
View File
@@ -45,7 +45,7 @@ def test_offline_shell_contains_every_local_dashboard_runtime_asset():
shell_assets = set(re.findall(r"BASE \+ '([^']+)'", worker.split("async function sessionCsrf", 1)[0]))
assert local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}"
assert "stackchain-dashboard-shell-v127" in worker
assert "stackchain-dashboard-shell-v128" in worker
def test_all_conversation_composers_offer_accessible_mobile_mentions():
+1 -1
View File
@@ -383,7 +383,7 @@ def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
assert "controller.recoverPermission('deadline')" in dashboard
assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard
assert "BASE + 'static/mobile-device-setup.js'" in worker
assert "stackchain-dashboard-shell-v127" in worker
assert "stackchain-dashboard-shell-v128" in worker
assert ".device-setup-panel" in css
assert ".device-readiness-card" in css
assert "overflow-x:hidden" in css
+1 -1
View File
@@ -243,5 +243,5 @@ async def test_mobile_home_progressively_discloses_secondary_panels_as_insights(
def test_mobile_insights_rolls_into_the_offline_shell():
worker = (CONTROLLER.parent / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v127" in worker
assert "stackchain-dashboard-shell-v128" in worker
assert "BASE + 'static/mobile-insights.js'" in worker
+1 -1
View File
@@ -358,7 +358,7 @@ async def test_dashboard_wires_thumb_safe_start_day_briefing_into_offline_mobile
assert ".mobile-start-day-finish { min-height:44px;" in html
assert "max-width:100%; overflow-wrap:anywhere;" in html
assert "BASE + 'static/mobile-start-day.js'" in service_worker
assert "stackchain-dashboard-shell-v127" in service_worker
assert "stackchain-dashboard-shell-v128" in service_worker
@pytest.mark.anyio
+1 -1
View File
@@ -410,7 +410,7 @@ async def test_plan_today_wires_cancel_back_and_success_through_overlay_history(
def test_plan_today_controller_is_available_in_the_offline_shell():
source = SERVICE_WORKER.read_text()
assert "stackchain-dashboard-shell-v127" in source
assert "stackchain-dashboard-shell-v128" in source
assert "BASE + 'static/plan-today.js'" in source
assert "BASE + 'static/plan-today-readiness.js'" in source
assert "BASE + 'static/plan-today-preview.js'" in source
+15 -15
View File
@@ -180,20 +180,20 @@ async function dispatchPush(payload) {{
def test_private_today_action_mailbox_rolls_the_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v127" in source
assert "stackchain-dashboard-shell-v128" in source
def test_per_day_week_conflict_ui_rolls_the_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v127" in source
assert "stackchain-dashboard-shell-v128" in source
assert "BASE + 'static/week-plan.js'" in source
def test_resumable_today_session_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v127" in source
assert "stackchain-dashboard-shell-v128" in source
assert "BASE + 'static/my-work.js'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/dashboard.css'" in source
@@ -202,7 +202,7 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v127" in source
assert "stackchain-dashboard-shell-v128" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/authored-outbox.js'" in source
assert "BASE + 'static/background-issue-sync.js'" in source
@@ -211,7 +211,7 @@ def test_mobile_conversation_photo_bundles_roll_the_offline_shell():
def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v127" in source
assert "stackchain-dashboard-shell-v128" in source
assert "BASE + 'static/issue-evidence-review.js'" in source
assert "BASE + 'static/issue-attachment.js'" in source
@@ -219,14 +219,14 @@ def test_photo_metadata_sanitizer_rolls_the_cached_optimizer_atomically():
def test_ownership_exit_runtime_rolls_the_offline_shell_cache():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v127" in source
assert "stackchain-dashboard-shell-v128" in source
assert "BASE + 'static/dashboard.js'" in source
def test_offline_review_next_ships_today_completion_atomically():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v127" in source
assert "stackchain-dashboard-shell-v128" in source
assert "BASE + 'static/today-completion.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -234,7 +234,7 @@ def test_offline_review_next_ships_today_completion_atomically():
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v127" in source
assert "stackchain-dashboard-shell-v128" in source
assert "BASE + 'static/create-issue-sheet.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -242,7 +242,7 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v127" in source
assert "stackchain-dashboard-shell-v128" in source
assert "BASE + 'static/issue-sheet.js'" in source
assert "BASE + 'static/checklist-conflict.js'" in source
assert "BASE + 'static/dashboard.js'" in source
@@ -252,14 +252,14 @@ def test_inline_checklist_step_flow_rolls_the_offline_shell_atomically():
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v127" in source
assert "stackchain-dashboard-shell-v128" in source
assert "BASE + 'static/later-picker.js'" in source
def test_navigation_deadline_ships_in_a_new_shell_cache():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v127" in source
assert "stackchain-dashboard-shell-v128" in source
assert "BASE + 'static/dashboard.css'" in source
assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/install-app.js'" in source
@@ -268,21 +268,21 @@ def test_navigation_deadline_ships_in_a_new_shell_cache():
def test_today_convergence_ships_in_a_new_shell_cache():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v127" in source
assert "stackchain-dashboard-shell-v128" in source
assert "BASE + 'static/today-sync.js'" in source
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v127" in source
assert "stackchain-dashboard-shell-v128" in source
assert "BASE + 'static/mobile-search-viewport.js'" in source
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v127" in source
assert "stackchain-dashboard-shell-v128" in source
assert "BASE + 'static/update-ownership.js'" in source
@@ -1252,7 +1252,7 @@ def test_one_session_bound_csrf_proof_is_reused_for_a_background_drain():
def test_queue_today_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v127" in source
assert "stackchain-dashboard-shell-v128" in source
assert "BASE + 'static/queue-today.js'" in source
+1 -1
View File
@@ -221,7 +221,7 @@ async def test_today_blocker_opens_existing_preview_and_preserves_readiness_gate
def test_readiness_runtime_is_available_in_offline_shell():
service_worker = SERVICE_WORKER.read_text()
assert "const CACHE = 'stackchain-dashboard-shell-v127';" in service_worker
assert "const CACHE = 'stackchain-dashboard-shell-v128';" in service_worker
assert "BASE + 'static/today-readiness.js'" in service_worker
+1 -1
View File
@@ -343,7 +343,7 @@ listeners['stackchain:first-task-complete']();
def test_inflight_today_drain_ships_in_a_new_offline_shell():
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v127" in source
assert "stackchain-dashboard-shell-v128" in source
assert "BASE + 'static/today-sync.js'" in source
+23
View File
@@ -51,6 +51,29 @@ def test_week_plan_is_encrypted_account_scoped_revisioned_and_preserves_other_da
assert conflict.value.snapshot == updated
def test_week_plan_rejects_the_same_work_on_multiple_dates_without_advancing_revision(tmp_path):
store = TodayStore(tmp_path / "today.sqlite3", encryption_key=b"w" * 32)
original = store.replace_week(
"timmy", base_revision=0, days=sample_days(), timezone="UTC"
)
duplicated = [
sample_days()[0],
{
"plan_date": "2026-08-22",
"ids": ["issue:secret/repo:3:"],
"capacity_minutes": 60,
"estimates": {"issue:secret/repo:3:": 30},
},
]
with pytest.raises(ValueError, match="work must be assigned to only one Week Ahead date"):
store.replace_week(
"timmy", base_revision=original["revision"], days=duplicated, timezone="UTC"
)
assert store.get_week("timmy") == original
def test_week_promotion_moves_only_due_date_to_today_exactly_once(tmp_path):
store = TodayStore(
tmp_path / "today.sqlite3",
+114
View File
@@ -67,6 +67,34 @@ console.log(JSON.stringify({first,middle,last}));
}
def test_week_controller_reviews_capacity_duplicates_and_moves_work_without_copying():
result = run_controller("""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson:async()=>({}),
localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:6,timezone:'UTC',days:[
{plan_date:'2026-08-21',ids:['shared','one'],capacity_minutes:45,estimates:{shared:30,one:30}},
{plan_date:'2026-08-22',ids:['shared','two'],capacity_minutes:120,estimates:{shared:30,two:40}}
]});
const before=week.review();
const moved=week.move('shared','2026-08-23');
const after=week.review();
console.log(JSON.stringify({before,moved,after,pending:week.pending()}));
""")
assert len(result["before"]["days"]) == 7
assert result["before"]["days"][0]["planned_minutes"] == 60
assert result["before"]["days"][0]["overloaded"] is True
assert result["before"]["duplicates"] == [{
"id": "shared", "dates": ["2026-08-21", "2026-08-22"]
}]
assert result["moved"] is True
assert result["after"]["duplicates"] == []
assert [day["ids"] for day in result["after"]["days"][:3]] == [["one"], ["two"], ["shared"]]
assert result["pending"]["days"][2]["estimates"] == {"shared": 30}
def test_week_workflow_saves_and_advances_without_closing_the_planner():
result = run_controller("""
const createWorkflow=createWeekPlan.Workflow;
@@ -104,6 +132,47 @@ console.log(JSON.stringify({continued,selected:workflow.selectedDate(),opened,st
assert result["label"] == "Save & next"
def test_week_workflow_opens_review_after_day_seven_and_blocks_duplicate_confirmation():
result = run_controller("""
const createWorkflow=createWeekPlan.Workflow;
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
const elements=new Map();
const makeElement=()=>({hidden:false,textContent:'',disabled:false,innerHTML:'',addEventListener:()=>{},focus:()=>{},
querySelectorAll:()=>[],querySelector:()=>null,scrollIntoView:()=>{}});
const root=makeElement();
Object.defineProperty(root,'innerHTML',{set(value){this.value=value;this.buttons=[];},get(){return this.value||'';}});
elements.set('#week-plan-dates',root);
for(const selector of ['#mobile-week-summary','#my-work-action-status','#week-plan-progress','#save-today-plan',
'#week-review','#week-review-days','#week-review-duplicates','#confirm-week-plan','#week-review-status']) elements.set(selector,makeElement());
let reviewMode=false;
const review={days:dates.map((date,index)=>({plan_date:date,label:'Day '+(index+1),ids:index<2?['shared']:[],
capacity_minutes:index===0?30:60,planned_minutes:index===0?45:15,overloaded:index===0})),
duplicates:[{id:'shared',dates:dates.slice(0,2)}],can_confirm:false};
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),day:date=>review.days.find(day=>day.plan_date===date),
pass:date=>{const index=dates.indexOf(date);return {position:index+1,total:7,planned:2,next_date:dates[index+1]||null,last:index===6};},
load:async()=>({}),summary:()=> '2 items across 2 days',review:()=>review,move:()=>true,flush:async()=>({})};
const workflow=createWorkflow({controller,qs:selector=>elements.get(selector),getLogin:()=> 'timmy',
openPlanner:()=>{},setReviewMode:value=>{reviewMode=value;},escapeHtml:value=>value,escapeAttribute:value=>value,
todayWork:{replace:()=>{},replacePlanning:()=>{}},refresh:()=>{},warm:()=>{}});
await workflow.open({disabled:false});
for(let index=0;index<6;index++) workflow.advance();
const advanced=workflow.advance();
console.log(JSON.stringify({advanced,reviewing:workflow.reviewing(),reviewMode,
reviewHidden:elements.get('#week-review').hidden,progress:elements.get('#week-plan-progress').textContent,
confirmDisabled:elements.get('#confirm-week-plan').disabled,days:elements.get('#week-review-days').innerHTML,
duplicates:elements.get('#week-review-duplicates').innerHTML}));
""")
assert result["advanced"] is True
assert result["reviewing"] is True
assert result["reviewMode"] is True
assert result["reviewHidden"] is False
assert result["progress"] == "Review week · 2 planned"
assert result["confirmDisabled"] is True
assert "45 of 30 min · over capacity" in result["days"]
assert "shared" in result["duplicates"]
def test_week_controller_preserves_both_versions_when_another_device_changes_the_week():
result = run_controller("""
let calls=0;
@@ -260,6 +329,35 @@ console.log(JSON.stringify({requests,during,saved,reused,pendingAfter:week.pendi
assert result["state"]["revision"] == 9
def test_week_controller_drains_work_staged_while_a_save_is_in_flight():
result = run_controller("""
const values=new Map();
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)};
let releaseFirst;const bodies=[];
const fetchJson=async(_url,options={})=>{
const body=JSON.parse(options.body);bodies.push(body);
if(bodies.length===1)await new Promise(resolve=>{releaseFirst=resolve});
return {revision:8+bodies.length,timezone:body.timezone,days:body.days};
};
const week=createWeekPlan({storage,getLogin:()=> 'timmy',fetchJson,localDate:()=> '2026-08-20',timeZone:()=> 'UTC'});
week.adopt({revision:8,timezone:'UTC',days:[]});
week.stageDay('2026-08-21',{ids:['one'],capacity_minutes:60,estimates:{one:30}});
const first=week.flush();await Promise.resolve();
week.stageDay('2026-08-22',{ids:['two'],capacity_minutes:90,estimates:{two:45}});
const second=week.flush();releaseFirst();
const [saved,reused]=await Promise.all([first,second]);
console.log(JSON.stringify({bodies,saved,reused,pending:week.pending(),state:week.state()}));
""")
assert len(result["bodies"]) == 2
assert [day["plan_date"] for day in result["bodies"][1]["days"]] == ["2026-08-21", "2026-08-22"]
assert result["bodies"][1]["base_revision"] == 9
assert result["saved"]["revision"] == 10
assert result["reused"]["revision"] == 10
assert result["pending"] is False
assert result["state"]["revision"] == 10
def test_week_controller_keeps_phone_week_by_rebasing_onto_the_latest_revision():
result = run_controller("""
const values=new Map();
@@ -423,6 +521,22 @@ def test_mobile_week_ahead_entry_and_date_strip_are_touch_safe():
assert "overflow-x:auto" in css
def test_mobile_week_ahead_review_is_rendered_touch_safe_and_confirmed_explicitly():
index = (FRONTEND / "index.html").read_text()
css = (FRONTEND / "dashboard.css").read_text()
dashboard = (FRONTEND / "dashboard.js").read_text()
assert 'id="week-review"' in index
assert 'id="week-review-days"' in index
assert 'id="week-review-duplicates"' in index
assert 'id="confirm-week-plan"' in index
assert "setReviewMode:value=>qs('#plan-today-sheet').classList.toggle('week-review-mode',value)" in dashboard
assert "weekWorkflow.confirm()" in dashboard
assert ".week-review-day.is-overloaded" in css
assert ".week-review-day button { min-height:44px;" in css
assert ".week-review-mode .mobile-plan-today-nav" in css
def test_mobile_week_ahead_exposes_durable_save_and_touch_safe_conflict_choices():
index = (FRONTEND / "index.html").read_text()
css = (FRONTEND / "dashboard.css").read_text()