feat: import private calendar capacity for Week Ahead (Closes #1206)
CI / lint (pull_request) Successful in 4m1s
CI / build-release (pull_request) Successful in 8s
CI / browser-journey (pull_request) Successful in 5m19s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-21 03:16:40 +00:00
1 parent aa46196d1e
commit e77ad7c809
12 files changed
+336 -6

No files matched your search

+35 -1
View File
@@ -97,6 +97,40 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
page.locator("#back-to-week-review").click()
expect(page.locator("#week-review")).to_be_visible()
expect(first_day).to_contain_text("Ship mobile capture")
page.locator("#open-week-capacity-import").click()
capacity_import = page.locator("#week-capacity-import")
expect(capacity_import).to_be_visible()
private_title = "Private customer planning"
tomorrow = (date.today() + timedelta(days=1)).strftime("%Y%m%d")
calendar = (
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\n"
f"SUMMARY:{private_title}\r\nDTSTART:{tomorrow}T100000\r\n"
f"DTEND:{tomorrow}T110000\r\nEND:VEVENT\r\nEND:VCALENDAR"
)
page.locator("#week-capacity-file").set_input_files({
"name": "availability.ics", "mimeType": "text/calendar", "buffer": calendar.encode()
})
expect(page.locator("#week-capacity-days .week-capacity-day")).to_have_count(7)
expect(page.locator("#week-capacity-days .week-capacity-day").first).to_contain_text(
"420 min available"
)
expect(capacity_import).not_to_contain_text(private_title)
for control in (
page.locator("#cancel-week-capacity-import"),
page.locator("#week-capacity-file"),
page.locator("#week-capacity-start"),
page.locator("#week-capacity-end"),
page.locator("#apply-week-capacities"),
):
bounds = control.bounding_box()
assert bounds and bounds["height"] >= 44
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
page.locator("#apply-week-capacities").click()
expect(capacity_import).to_be_hidden()
expect(page.locator("#week-review")).to_be_visible()
expect(first_day).to_contain_text("30 of 420 min")
page.wait_for_function("() => !document.querySelector('#confirm-week-plan').disabled")
confirm = page.locator("#confirm-week-plan")
bounds = confirm.bounding_box()
@@ -118,7 +152,7 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
assert download_info.value.suggested_filename.startswith("stackchain-week-ahead-")
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"]) == 3
assert saved and len(saved[-1]["days"]) == 7
assert [day["plan_date"] for day in saved[-1]["days"]] == sorted(
day["plan_date"] for day in saved[-1]["days"]
)
+1
View File
@@ -1329,6 +1329,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
"/dashboard/static/plan-today-preview.js",
"/dashboard/static/tomorrow-plan.js",
"/dashboard/static/week-calendar.js",
"/dashboard/static/week-calendar-import.js",
"/dashboard/static/week-plan.js",
"/dashboard/static/search-week-plan.js",
"/dashboard/static/today-sync.js",
+1 -1
View File
@@ -107,4 +107,4 @@ def test_week_calendar_handoff_is_wired_into_confirmation_with_mobile_privacy_co
assert "mountWeekCalendarHandoff" in WEEK_CALENDAR.read_text()
assert "weekCalendar.open(weekPlan.state())" in dashboard
assert "weekCalendar.close()" in dashboard
assert '"static/week-calendar.js", "static/week-plan.js"' in bundle
assert '"static/week-calendar.js", "static/week-calendar-import.js", "static/week-plan.js"' in bundle
+108
View File
@@ -0,0 +1,108 @@
import json
import subprocess
from pathlib import Path
MODULE = Path(__file__).parents[1] / "frontend" / "week-calendar-import.js"
FRONTEND = MODULE.parent
def run_import(scenario: str) -> dict:
harness = f"""
const calendarImport = require({json.dumps(str(MODULE))});
(async() => {{ {scenario} }})().catch(error=>{{console.error(error);process.exit(1);}})
"""
completed = subprocess.run(
["node", "-e", harness], check=True, capture_output=True, text=True
)
return json.loads(completed.stdout)
def test_calendar_import_unions_busy_time_within_working_hours_across_local_days():
result = run_import("""
const source=`BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:one\r\nSUMMARY:Private planning\r\nDTSTART:20260821T100000\r\nDTEND:20260821T110000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:two\r\nSUMMARY:Secret customer call\r\nATTENDEE:mailto:private@example.com\r\nDTSTART:20260821T103000\r\nDTEND:20260821T120000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:outside\r\nDTSTART:20260821T070000\r\nDTEND:20260821T080000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nUID:overnight\r\nLOCATION:Private office\r\nDTSTART:20260821T163000\r\nDTEND:20260822T100000\r\nEND:VEVENT\r\nEND:VCALENDAR`;
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
const days=calendarImport.review(source,{dates,workdayStart:'09:00',workdayEnd:'17:00'});
console.log(JSON.stringify({days,serialized:JSON.stringify(days)}));
""")
assert result["days"][0] == {
"plan_date": "2026-08-21", "busy_minutes": 150, "capacity_minutes": 330
}
assert result["days"][1] == {
"plan_date": "2026-08-22", "busy_minutes": 60, "capacity_minutes": 420
}
assert [day["capacity_minutes"] for day in result["days"][2:]] == [480] * 5
for private_value in ("Private planning", "Secret customer call", "private@example.com", "Private office"):
assert private_value not in result["serialized"]
def test_calendar_import_rejects_invalid_and_oversized_files_without_returning_capacity():
result = run_import("""
const errors=[];
for(const source of ['not a calendar','BEGIN:VCALENDAR\\r\\n'+('X'.repeat(1024*1024))+'\\r\\nEND:VCALENDAR']) {
try { calendarImport.review(source,{dates:['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27']}); }
catch(error) { errors.push(error.message); }
}
console.log(JSON.stringify({errors}));
""")
assert result["errors"] == [
"Choose a valid .ics calendar file.",
"Calendar files must be 1 MB or smaller.",
]
def test_calendar_import_workflow_reviews_then_applies_once_without_persisting_calendar_text():
result = run_import("""
const elements=new Map();
const element=(value='')=>({value,hidden:false,textContent:'',innerHTML:'',disabled:false,files:[],listeners:{},
addEventListener(name,listener){this.listeners[name]=listener;},focus(){this.focused=true;}});
for(const selector of ['#week-capacity-import','#week-capacity-review','#week-capacity-days','#week-capacity-status',
'#week-capacity-file','#week-capacity-start','#week-capacity-end','#apply-week-capacities','#cancel-week-capacity-import'])
elements.set(selector,element());
elements.get('#week-capacity-start').value='09:00';elements.get('#week-capacity-end').value='17:00';
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
let staged=[],flushes=0,appliedCalls=0;
const controller={dates:()=>dates.map((date,index)=>({date,label:'Day '+(index+1)})),
stageCapacities:value=>{staged.push(value);return {sync_pending:true};},flush:async()=>{flushes+=1;}};
const workflow=calendarImport.createWorkflow({controller,qs:selector=>elements.get(selector),onApplied:()=>{appliedCalls+=1;}});
workflow.open();
const source=`BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nSUMMARY:Do not retain me\r\nDTSTART:20260821T100000\r\nDTEND:20260821T110000\r\nEND:VEVENT\r\nEND:VCALENDAR`;
const reviewed=workflow.review(source);
await workflow.apply();
console.log(JSON.stringify({reviewed,staged,flushes,appliedCalls,rootHidden:elements.get('#week-capacity-import').hidden,
status:elements.get('#week-capacity-status').textContent,markup:elements.get('#week-capacity-days').innerHTML,
fileValue:elements.get('#week-capacity-file').value,workflowState:workflow.state()}));
""")
assert result["reviewed"][0]["capacity_minutes"] == 420
assert len(result["staged"]) == 1
assert len(result["staged"][0]) == 7
assert result["flushes"] == 1
assert result["appliedCalls"] == 1
assert result["rootHidden"] is True
assert result["fileValue"] == ""
assert result["workflowState"] is None
assert "Do not retain me" not in json.dumps(result)
def test_week_ahead_exposes_private_calendar_capacity_import_in_the_packaged_planning_flow():
html = (FRONTEND / "index.html").read_text()
dashboard = (FRONTEND / "dashboard.js").read_text()
css = (FRONTEND / "dashboard.css").read_text()
assert 'id="open-week-capacity-import"' in html
assert 'id="week-capacity-import"' in html
assert 'id="week-capacity-file"' in html and 'accept=".ics,text/calendar"' in html
assert 'id="week-capacity-start"' in html and 'id="week-capacity-end"' in html
assert 'id="week-capacity-review"' in html
assert 'id="apply-week-capacities"' in html
assert '<script src="static/week-calendar-import.js"></script>' in html
assert html.index('static/week-calendar-import.js') < html.index('static/week-plan.js')
week_plan = (FRONTEND / "week-plan.js").read_text()
assert "weekCalendarImport.mount" in week_plan
assert "weekCalendarImport.mount" not in dashboard
assert "open-week-capacity-import" in MODULE.read_text()
assert ".week-capacity-import" in css
assert "overflow-x:hidden" in css
+25
View File
@@ -114,6 +114,31 @@ console.log(JSON.stringify({before,moved,after,pending:week.pending()}));
assert result["pending"]["days"][2]["estimates"] == {"shared": 30}
def test_week_controller_applies_seven_imported_capacities_in_one_local_transition():
result = run_controller("""
const values=new Map();let writes=0;
const storage={getItem:key=>values.get(key)||null,setItem:(key,value)=>{writes+=1;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:['one'],capacity_minutes:60,estimates:{one:30}},
{plan_date:'2026-08-23',ids:['three'],capacity_minutes:90,estimates:{three:45}}
]});
const capacities=week.dates().map((item,index)=>({plan_date:item.date,capacity_minutes:300+index*15}));
const applied=week.stageCapacities(capacities);
console.log(JSON.stringify({applied,writes,state:week.state(),pending:week.pending()}));
""")
assert result["writes"] == 1
assert result["applied"]["sync_pending"] is True
assert [day["capacity_minutes"] for day in result["state"]["days"]] == [
300, 315, 330, 345, 360, 375, 390
]
assert result["state"]["days"][0]["ids"] == ["one"]
assert result["state"]["days"][2]["ids"] == ["three"]
assert result["pending"]["days"] == result["state"]["days"]
def test_week_controller_places_new_work_or_explicitly_moves_existing_work_without_duplicates():
result = run_controller("""
const values=new Map();