diff --git a/frontend/dashboard.css b/frontend/dashboard.css index 8145d72..5f1f18a 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -100,6 +100,14 @@ textarea { resize: vertical; min-height: 120px; } .plan-preview-actions button { min-height:44px; } .plan-today-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; } .plan-today-actions button { min-height:44px; width:100%; } +.today-readiness-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-readiness-sheet[hidden] { display:none; } +.today-readiness-panel { box-sizing:border-box; width:min(620px,100%); max-height:100%; overflow:auto; overflow-x:hidden; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); border:1px solid #b45309; border-radius:18px 18px 0 0; background:#0b1526; } +.today-readiness-header { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; } +.today-readiness-header h2 { margin:.2rem 0; } +.today-readiness-header button, .today-readiness-actions button { min-height:44px; } +.today-readiness-item { min-width:0; overflow-wrap:anywhere; margin:12px 0; padding:12px; border:1px solid #31577f; border-radius:12px; background:#10233a; } +.today-readiness-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; } .my-work-header { display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap; } .work-settings { width:100%; } .work-settings > summary { display:none; } diff --git a/frontend/dashboard.js b/frontend/dashboard.js index 1a80761..d844d88 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -728,25 +728,105 @@ qs('[data-work-filter="today"]').click(); } - function continueTodaySession() { - selectTodayWork(); - if (!workSession.reopen() && !workSession.reconcile()) { - qs('#my-work-action-status').textContent = 'Current Today item is no longer available.'; + let todayReadinessTrigger = null; + function inspectTodayDependencies(item) { + if (offlineWorkMode) { + const login = confirmedOwnerLogin || String(offlineWorkStore.load()?.user?.login || '').trim(); + const detail = offlineWorkStore.loadDetail(login, item); + return Promise.resolve({ + available:detail?.dependencies_available === true, + dependencies:Array.isArray(detail?.dependencies) ? detail.dependencies : [], + }); + } + const [owner, repo] = String(item.repository || '').split('/'); + return api('api/v1/repos/' + encodeURIComponent(owner) + '/' + encodeURIComponent(repo) + + '/issues/' + encodeURIComponent(item.number) + '/detail').then(detail => ({ + available:detail?.dependencies_available === true, + dependencies:Array.isArray(detail?.dependencies) ? detail.dependencies : [], + })); + } + + function closeTodayReadiness(navigate = true) { + qs('#today-readiness-sheet').hidden = true; + document.body.classList.remove('task-overlay-open'); + if (navigate) taskOverlayHistory.close(); + else { + todayReadiness.cancel(); + requestAnimationFrame(() => todayReadinessTrigger?.focus()); + } + } + + function renderTodayReadiness(state) { + if (!state?.target) return; + const unknown = state.status === 'unknown'; + qs('#today-readiness-title').textContent = unknown ? 'Blocker status unavailable' : 'This item is blocked'; + qs('#today-readiness-summary').textContent = unknown + ? 'Stackchain could not confirm whether this issue is ready. Retry or choose an explicit override.' + : state.dependencies.length + (state.dependencies.length === 1 ? ' open dependency must finish first.' : ' open dependencies must finish first.'); + qs('#today-readiness-item').innerHTML = '' + escapeHtml(state.target.title || 'Untitled issue') + + '
' + escapeHtml(state.target.repository + '#' + state.target.number) + '
'; + qs('#today-readiness-blockers').innerHTML = state.dependencies.map(blocker => + '' + + escapeHtml(blocker.repository + '#' + blocker.number) + ' · ' + escapeHtml(blocker.title || 'Untitled blocker') + + 'State: ' + escapeHtml(blocker.state || 'open') + '' + ).join(''); + qs('#today-readiness-next').hidden = !state.nextReady; + if (state.nextReady) qs('#today-readiness-next').textContent = 'Start next ready · ' + + (state.nextReady.title || state.nextReady.key || 'work item'); + qs('#today-readiness-retry').hidden = !unknown; + qs('#today-readiness-sheet').hidden = false; + document.body.classList.add('task-overlay-open'); + requestAnimationFrame(() => (state.nextReady ? qs('#today-readiness-next') : qs('#today-readiness-anyway')).focus()); + } + + function performTodayTransition(action, item = null) { + qs('#today-readiness-sheet').hidden = true; + document.body.classList.remove('task-overlay-open'); + if (taskOverlayHistory.current?.() === 'today-readiness') taskOverlayHistory.close(); + const transitioned = ({ + start: () => workSession.start(item), + resume: () => workSession.resume(item), + continue: () => workSession.reopen(item), + next: () => workSession.next(item), + complete: () => workSession.complete(item), + })[action]?.(); + if (!transitioned && action !== 'next' && action !== 'complete') { + qs('#my-work-action-status').textContent = 'Saved Today item is no longer available.'; updateWorkSessionActions(); } } + const todayReadiness = createTodayReadiness({ + inspect:inspectTodayDependencies, + onOpen:performTodayTransition, + onGate:state => { + todayReadinessTrigger = document.activeElement; + renderTodayReadiness(state); + taskOverlayHistory.open('today-readiness'); + }, + }); + + function runTodayTransition(action) { + selectTodayWork(); + const target = workSession.target(action); + if (!target) { + performTodayTransition(action); + return Promise.resolve('empty'); + } + qs('#my-work-action-status').textContent = 'Checking Today readiness…'; + return todayReadiness.run(action, workSession.items(), target); + } + + function continueTodaySession() { + return runTodayTransition('continue'); + } + function resumeTodaySession() { - selectTodayWork(); - if (!workSession.resume()) { - qs('#my-work-action-status').textContent = 'Saved Today session is no longer available.'; - updateWorkSessionActions(); - } + return runTodayTransition('resume'); } function startTodaySession() { - selectTodayWork(); - workSession.start(); + return runTodayTransition('start'); } const createAndStart = createCreateAndStart({ @@ -781,6 +861,7 @@ refresh: () => refreshMyWorkView({ reconcileSession:false }), warm: warmTodayOffline, announce: message => { qs('#my-work-action-status').textContent = message; }, + advance: () => runTodayTransition('complete'), }); function setCommentNextVisibility(kind) { const item = kind === 'issue' ? selectedIssue : selectedPull; @@ -943,7 +1024,7 @@ save: saveTodayPlan, start: () => { qs('[data-work-filter="today"]').click(); - workSession.start(); + startTodaySession(); }, }); @@ -2996,10 +3077,12 @@ addPlanPreviewOverride = false; } if (previous === 'plan-today' && kind !== 'plan-today' && kind !== 'plan-today-preview') closePlanToday(false); + if (previous === 'today-readiness' && kind !== 'today-readiness') closeTodayReadiness(false); if (kind === 'new' && previous !== 'new') openCreateIssueSheet(false); if (kind === 'find' && previous !== 'find') openFindWorkSheet(false); if (kind === 'search' && previous !== 'search-preview') openCommandPalette(false); if (kind === 'plan-today' && previous !== 'plan-today' && previous !== 'plan-today-preview') openPlanToday(planTodayTrigger, false); + if (kind === 'today-readiness' && previous !== 'today-readiness') renderTodayReadiness(todayReadiness.snapshot()); }, }); taskOverlayHistory.start(); @@ -3630,9 +3713,11 @@ ); refreshMyWorkView({ reconcileSession:false }); const continuingSession = workSession.active(); - if (workSession.active()) workSession.complete(); - if (continuingSession && workSession.active()) { + const transitionResult = workSession.active() ? await runTodayTransition('complete') : null; + if (transitionResult === 'opened') { qs('#my-work-action-status').textContent = closing.key + ' closed. Next work item opened.'; + } else if (transitionResult === 'gated') { + qs('#my-work-action-status').textContent = closing.key + ' closed. Choose the next ready Today item.'; } else if (!continuingSession) { qs('#my-work-action-status').textContent = closing.key + ' closed.'; } @@ -3722,9 +3807,11 @@ ); refreshMyWorkView({ reconcileSession:false }); const continuingSession = workSession.active(); - if (workSession.active()) workSession.complete(); - if (continuingSession && workSession.active()) { + const transitionResult = workSession.active() ? await runTodayTransition('complete') : null; + if (transitionResult === 'opened') { qs('#my-work-action-status').textContent = merging.key + ' merged. Next work item opened.'; + } else if (transitionResult === 'gated') { + qs('#my-work-action-status').textContent = merging.key + ' merged. Choose the next ready Today item.'; } else if (!continuingSession) { qs('#my-work-action-status').textContent = merging.key + ' merged.'; } @@ -3891,7 +3978,7 @@ qs('#continue-review-to-merge').focus(); } else { await load(); - if (workSession.active()) workSession.complete(); + if (workSession.active()) await runTodayTransition('complete'); else button.focus(); } } catch (error) { @@ -4083,6 +4170,17 @@ qs('#plan-today-sheet').addEventListener('click', event => { if (event.target === qs('#plan-today-sheet')) closePlanToday(); }); + qs('#today-readiness-close').addEventListener('click', () => closeTodayReadiness()); + qs('#today-readiness-sheet').addEventListener('click', event => { + if (event.target === qs('#today-readiness-sheet')) closeTodayReadiness(); + }); + qs('#today-readiness-next').addEventListener('click', () => todayReadiness.startNextReady()); + qs('#today-readiness-anyway').addEventListener('click', () => todayReadiness.workAnyway()); + qs('#today-readiness-retry').addEventListener('click', () => todayReadiness.retry()); + qs('#today-readiness-plan').addEventListener('click', () => { + closeTodayReadiness(); + openPlanToday(qs('#plan-today')); + }); qs('#save-today-plan').addEventListener('click', () => { const result = planToday.commit(); if (result === 'saved') taskOverlayHistory.leave(); @@ -4099,15 +4197,10 @@ qs('#my-work-action-status').textContent = 'No visible work to start.'; return; } - workSession.start(); - }); - qs('#resume-today-session').addEventListener('click', () => { - qs('[data-work-filter="today"]').click(); - if (!workSession.resume()) { - qs('#my-work-action-status').textContent = 'Saved Today session is no longer available.'; - updateWorkSessionActions(); - } + if (selectedWorkFilter === 'today') runTodayTransition('start'); + else workSession.start(); }); + qs('#resume-today-session').addEventListener('click', () => resumeTodaySession()); qs('#end-today-session').addEventListener('click', () => { workSession.end(); qs('#my-work-action-status').textContent = 'Today session ended. Your plan is unchanged.'; @@ -4117,7 +4210,7 @@ button.addEventListener('click', () => workSession.previous()) ); document.querySelectorAll('[data-work-session-next]').forEach(button => - button.addEventListener('click', () => workSession.next()) + button.addEventListener('click', () => workSession.checkpointed() ? runTodayTransition('next') : workSession.next()) ); document.querySelectorAll('[data-work-session-complete]').forEach(button => button.addEventListener('click', () => diff --git a/frontend/index.html b/frontend/index.html index 09d0535..813ac2c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -196,6 +196,24 @@ + +