diff --git a/frontend/dashboard.css b/frontend/dashboard.css index 321efca..e147e83 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -290,6 +290,8 @@ textarea { resize: vertical; min-height: 120px; } .today-progress-evidence .issue-evidence-note textarea { min-height:72px; } .today-progress-blocker { display:grid; gap:8px; margin-top:14px; padding:12px; border:1px solid #875f2a; border-radius:12px; background:#21180d; } .today-progress-blocker input, .today-progress-blocker button { box-sizing:border-box; width:100%; min-height:44px; } +.today-blocker-presets { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:8px; } +.today-blocker-presets button { min-width:0; padding-inline:6px; } .today-progress-blocker p { margin:0; } .today-progress-blocker .today-progress-blocker-recovery { padding:8px; border-radius:8px; background:#3a270d; color:#fde68a; font-weight:700; } .today-progress-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:12px; } @@ -297,6 +299,13 @@ textarea { resize: vertical; min-height: 120px; } .today-progress-panel .voice-conversation { margin-top:10px; } .today-progress-panel .voice-conversation-controls button, .today-progress-panel .voice-conversation-review-actions button { min-height:44px; } +.today-progress-panel.blocker-mode .today-progress-activity, +.today-progress-panel.blocker-mode .voice-conversation, +.today-progress-panel.blocker-mode .today-progress-evidence, +.today-progress-panel.blocker-mode .today-progress-update-help, +.today-progress-panel.blocker-mode .today-progress-actions { display:none; } +.today-progress-panel.blocker-mode textarea { min-height:112px; } +@media(max-width:359px) { .today-blocker-presets { grid-template-columns:1fr; } } @media(max-width:359px) { .today-progress-actions { grid-template-columns:1fr; } } .today-recap-sheet { position:fixed; inset:0; z-index:88; display:flex; align-items:flex-end; justify-content:center; background:rgba(5,12,21,.82); backdrop-filter:blur(4px); } .today-recap-sheet[hidden] { display:none; } @@ -1116,7 +1125,8 @@ textarea { resize: vertical; min-height: 120px; } .mobile-today-hud [data-mobile-today-complete] { grid-area:complete; } .mobile-today-hud [data-mobile-today-toggle] { grid-area:toggle; } .mobile-today-hud button { min-height:44px; max-width:100%; } - .mobile-today-hud [data-mobile-today-update] { grid-column:1 / -1; width:100%; } + .mobile-today-hud [data-mobile-today-update] { grid-column:1; width:100%; } + .mobile-today-hud [data-mobile-today-blocked] { grid-column:2; width:100%; } .mobile-today-hud [data-today-break-open] { grid-column:1 / -1; } .mobile-today-hud [data-work-session-adjust-plan] { grid-column:1 / -1; } diff --git a/frontend/index.html b/frontend/index.html index 382a27b..875c249 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1537,6 +1537,7 @@
+ @@ -1556,7 +1557,7 @@ - +
@@ -1600,12 +1601,17 @@ -

Save privately on this device, or post to the exact Gitea item. Today keeps running either way.

+

Save privately on this device, or post to the exact Gitea item. Today keeps running either way.

Blocked? +
+ + + +

Posts this update first, then defers the item and continues Today.

diff --git a/frontend/today-progress.js b/frontend/today-progress.js index 3e74c9f..7e2037a 100644 --- a/frontend/today-progress.js +++ b/frontend/today-progress.js @@ -309,11 +309,15 @@ function mountTodayProgressActivity(qs, fetchJson, actionSource = {}, render) { return activity; } -function createTodayProgressView({ progress, currentTarget, qs, photos, voice, activity, mentions, announce = () => {}, onAdmitted = () => {}, moveOn = null }) { +function createTodayProgressView({ progress, currentTarget, qs, photos, voice, activity, mentions, announce = () => {}, onAdmitted = () => {}, moveOn = null, now = () => new Date() }) { const sheet = qs('#today-progress-sheet'); + const panel = qs('.today-progress-panel'); + const title = qs('#today-progress-title'); const body = qs('#today-progress-body'); + const bodyLabel = qs('#today-progress-body-label'); const status = qs('#today-progress-status'); const launcher = qs('[data-mobile-today-update]'); + const blockerLauncher = qs('[data-mobile-today-blocked]'); const blockerButton = qs('#post-today-blocker'); const blockerReturn = qs('#today-blocker-return-at'); const blockerRecovery = qs('#today-progress-blocker-recovery'); @@ -342,6 +346,7 @@ function createTodayProgressView({ progress, currentTarget, qs, photos, voice, a const update = () => { const target = currentTarget(); launcher.hidden = !target; + if (blockerLauncher) blockerLauncher.hidden = !target; if (openedTarget && target?.identity !== openedTarget.identity) mentions?.dismiss?.(); }; const checkpoint = async () => { @@ -362,28 +367,50 @@ function createTodayProgressView({ progress, currentTarget, qs, photos, voice, a activity?.close?.(); mentions?.dismiss?.(); if (sheet.open) sheet.close(); + panel?.classList?.remove('blocker-mode'); openedTarget = null; }; - launcher.addEventListener('click', async () => { + const open = async blockerMode => { const target = currentTarget(); if (!target) return; mentions?.dismiss?.(); openedTarget = target; + panel?.classList?.[blockerMode ? 'add' : 'remove']('blocker-mode'); + if (title) title.textContent = blockerMode ? 'Report blocker' : 'Add progress update'; + if (bodyLabel) bodyLabel.textContent = blockerMode ? 'Blocker description' : 'Update'; qs('#today-progress-target').textContent = target.label + (target.title ? ' · ' + target.title : ''); body.value = progress.load(target.identity); showBlockerRecovery(progress.blockerRecovery?.(target.identity)); - status.textContent = 'Restoring saved photo evidence…'; sheet.showModal(); - try { - await voice?.open?.(target.identity); - await photos?.open?.(target); - await activity?.open?.(target); - status.textContent = ''; + if (!blockerMode) { + status.textContent = 'Restoring saved photo evidence…'; + try { + await voice?.open?.(target.identity); + await photos?.open?.(target); + await activity?.open?.(target); + status.textContent = ''; + } + catch (error) { status.textContent = error.message + ' You can retry by reopening this update.'; } } - catch (error) { status.textContent = error.message + ' You can retry by reopening this update.'; } + else status.textContent = ''; body.focus(); - }); + }; + launcher.addEventListener('click', () => open(false)); + blockerLauncher?.addEventListener('click', () => open(true)); + + const setReturn = preset => { + const date = new Date(now()); + if (preset === 'later-today') date.setHours(date.getHours() + 4); + else { + date.setDate(date.getDate() + 1); + date.setHours(9, 0, 0, 0); + } + blockerReturn.value = localDateTime(date); + }; + qs('[data-today-blocker-preset="later-today"]')?.addEventListener('click', () => setReturn('later-today')); + qs('[data-today-blocker-preset="tomorrow"]')?.addEventListener('click', () => setReturn('tomorrow')); + qs('[data-today-blocker-custom]')?.addEventListener('click', () => blockerReturn?.focus?.()); qs('#cancel-today-progress').addEventListener('click', async () => { if (await checkpoint()) close(); }); diff --git a/tests/e2e/test_mobile_today_handoff_release.py b/tests/e2e/test_mobile_today_handoff_release.py index 3d5f709..9f8f512 100644 --- a/tests/e2e/test_mobile_today_handoff_release.py +++ b/tests/e2e/test_mobile_today_handoff_release.py @@ -87,6 +87,26 @@ def test_release_artifact_plans_hands_off_and_opens_next_mobile_issue(tmp_path: if page.locator("#plan-today-sheet").is_visible(): page.locator("#cancel-plan-today").click() expect(page.locator("[data-mobile-today-hud]")).to_be_visible() + blocked = page.locator("[data-mobile-today-blocked]") + expect(blocked).to_be_visible() + blocked_bounds = blocked.bounding_box() + assert blocked_bounds and blocked_bounds["height"] >= 44 + blocked.click() + expect(page.locator("#today-progress-sheet")).to_be_visible() + expect(page.locator("#today-progress-title")).to_have_text("Report blocker") + expect(page.locator("#today-progress-target")).to_contain_text("Ship mobile capture") + expect(page.locator("#today-progress-body")).to_be_focused() + expect(page.locator("#today-progress-activity")).to_be_hidden() + expect(page.locator(".today-progress-evidence")).to_be_hidden() + for control in page.locator(".today-blocker-presets button, #post-today-blocker").all(): + bounds = control.bounding_box() + assert bounds and bounds["height"] >= 44 + page.locator('[data-today-blocker-preset="later-today"]').click() + assert page.locator("#today-blocker-return-at").input_value() + assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth") + page.locator("#cancel-today-progress").click() + expect(page.locator("#today-progress-sheet")).to_be_hidden() + expect(page.locator("[data-mobile-today-hud]")).to_contain_text("Ship mobile capture") fake.activity_comments = { 41: [{ "id": 701, diff --git a/tests/test_today_progress.py b/tests/test_today_progress.py index 09333f3..aab951b 100644 --- a/tests/test_today_progress.py +++ b/tests/test_today_progress.py @@ -329,6 +329,65 @@ const view=createView({{ } +def test_blocked_hud_opens_a_focused_flow_with_quick_return_choices(): + script = f""" +const {{createView}}=require({json.dumps(str(TODAY_PROGRESS))}); +class Element {{ + constructor() {{ + this.hidden=false;this.value='';this.textContent='';this.open=false;this.focused=false;this.listeners={{}}; + const names=new Set(); + this.classList={{add:name=>names.add(name),remove:name=>names.delete(name),contains:name=>names.has(name)}}; + }} + addEventListener(name,callback) {{ this.listeners[name]=callback; }} + click() {{ return this.listeners.click?.(); }} + showModal() {{ this.open=true; }} close() {{ this.open=false; }} focus() {{ this.focused=true; }} +}} +const selectors=['#today-progress-sheet','.today-progress-panel','#today-progress-title','#today-progress-body', + '#today-progress-status','[data-mobile-today-update]','[data-mobile-today-blocked]', + '#today-progress-target','#cancel-today-progress','#save-today-progress','#post-today-progress', + '#today-blocker-return-at','#post-today-blocker','[data-today-blocker-preset="later-today"]', + '[data-today-blocker-preset="tomorrow"]','[data-today-blocker-custom]']; +const elements=Object.fromEntries(selectors.map(selector=>[selector,new Element()])); +const target={{identity:'issue:stackchain/dashboard:1070:',kind:'issue',repository:'stackchain/dashboard',number:1070,label:'#1070',title:'Focused blocker'}}; +const calls=[]; +const view=createView({{ + progress:{{load:()=> 'Waiting for access',save:()=>true}},currentTarget:()=>target,qs:selector=>elements[selector], + voice:{{open:async()=>calls.push('voice')}},photos:{{open:async()=>calls.push('photos')}}, + activity:{{open:async()=>calls.push('activity')}},now:()=>new Date('2026-08-18T10:15:00Z'), +}}); +(async()=>{{ + const availability={{update:elements['[data-mobile-today-update]'].hidden,blocked:elements['[data-mobile-today-blocked]'].hidden}}; + await elements['[data-mobile-today-blocked]'].click(); + const opened={{ + title:elements['#today-progress-title'].textContent,target:elements['#today-progress-target'].textContent, + focused:elements['#today-progress-body'].focused,mode:elements['.today-progress-panel'].classList.contains('blocker-mode'), + calls:[...calls],sheet:elements['#today-progress-sheet'].open, + }}; + await elements['[data-today-blocker-preset="later-today"]'].click(); + const laterToday=elements['#today-blocker-return-at'].value; + await elements['[data-today-blocker-preset="tomorrow"]'].click(); + const tomorrow=elements['#today-blocker-return-at'].value; + elements['#today-blocker-return-at'].focused=false; + await elements['[data-today-blocker-custom]'].click(); + process.stdout.write(JSON.stringify({{availability,opened,laterToday,tomorrow,customFocused:elements['#today-blocker-return-at'].focused}})); +}})().catch(error=>{{console.error(error);process.exit(1);}}); +""" + assert run_node(script) == { + "availability": {"update": False, "blocked": False}, + "opened": { + "title": "Report blocker", + "target": "#1070 · Focused blocker", + "focused": True, + "mode": True, + "calls": [], + "sheet": True, + }, + "laterToday": "2026-08-18T14:15", + "tomorrow": "2026-08-19T09:00", + "customFocused": True, + } + + def test_progress_sheet_dismisses_mentions_when_opened_switched_or_closed(): script = f""" const {{createView}}=require({json.dumps(str(TODAY_PROGRESS))});