diff --git a/frontend/dashboard.js b/frontend/dashboard.js index 90566e7..e2d9d29 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -147,6 +147,7 @@ let lastContextSnapshot = null; let notificationPagination = { page: 1, total: 0, has_more: false }; let workPagination = {}; + let agendaChecking = false; let hasContextSnapshot = false; let selectedReview = null; let reviewTrigger = null; @@ -2202,6 +2203,9 @@ 'No assigned work, review requests, or unread updates.'; updateWorkPaginationControls(); renderMyWork(); + if (selectedWorkFilter === 'agenda' && workPagination.issue?.has_more && !agendaChecking) { + completeAgendaIssues(); + } if (reconcileSession && workSession.active()) workSession.reconcile(); updateWorkSessionActions(); } @@ -2217,6 +2221,20 @@ return []; } + async function completeAgendaIssues() { + if (selectedWorkFilter !== 'agenda' || !workPagination.issue?.has_more || !lastContextSnapshot) return; + agendaChecking = true; + qs('#my-work-action-status').textContent = 'Checking all assigned deadlines…'; + renderMyWork(); + const complete = await workPager.loadAll('issue', () => lastContextSnapshot?.issues || []); + agendaChecking = false; + if (selectedWorkFilter !== 'agenda') return; + qs('#my-work-action-status').textContent = complete ? + 'All assigned deadlines checked.' : + 'Agenda check paused. Retry to check older assigned deadlines.'; + renderMyWork(); + } + function renderDrafts() { const list = qs('#my-work-list'); const displayedDrafts = findQueueItems(lastDrafts, queueFindQuery); @@ -2489,9 +2507,12 @@ return '
' + selector + '' + contents + '' + readUpdate + markRead + planningActions + '
'; } return '
' + selector + '' + contents + '' + markRead + planningActions + '
'; - }).join('') : (showEmptyStart ? '' : '
' + (incomplete ? + }).join('') : (showEmptyStart ? '' : '
' + + (selectedWorkFilter === 'agenda' && workPagination.issue?.has_more ? + (agendaChecking ? 'Checking all assigned deadlines…' : + 'Older assigned deadlines remain unchecked. Retry the Agenda check.') : (incomplete ? 'More work is available. Load the next page.' : - 'No ' + (selectedWorkFilter === 'attention' ? 'items need attention' : (selectedWorkFilter === 'review' ? 'reviews' : (selectedWorkFilter === 'update' ? 'unread updates' : (selectedWorkFilter === 'later' ? 'deferred work' : (selectedWorkFilter === 'all' ? 'work' : selectedWorkFilter + ' items'))))) + '.') + '
'); + 'No ' + (selectedWorkFilter === 'attention' ? 'items need attention' : (selectedWorkFilter === 'review' ? 'reviews' : (selectedWorkFilter === 'update' ? 'unread updates' : (selectedWorkFilter === 'later' ? 'deferred work' : (selectedWorkFilter === 'all' ? 'work' : selectedWorkFilter + ' items'))))) + '.')) + '
'); cardPlanning.wire(); document.querySelectorAll('[data-select-notification-id]').forEach(input => { input.addEventListener('change', () => { @@ -5778,6 +5799,10 @@ notificationPager.loadMore(lastNotifications) ); qs('#load-more-work').addEventListener('click', async () => { + if (selectedWorkFilter === 'agenda') { + await completeAgendaIssues(); + return; + } const stream = activeWorkStreams().find(item => workPagination[item]?.has_more); if (!stream || !lastContextSnapshot) return; const button = qs('#load-more-work'); @@ -5931,6 +5956,7 @@ qs('#active-work-queue').textContent = button.firstChild.textContent.trim() + ' (' + selectedCount + ')'; renderMyWork(); updateWorkPaginationControls(); + if (selectedWorkFilter === 'agenda') completeAgendaIssues(); if (!preserveRoute) workRoute.queue(filter); return true; } diff --git a/frontend/my-work.js b/frontend/my-work.js index 06a72a3..ca8a4a1 100644 --- a/frontend/my-work.js +++ b/frontend/my-work.js @@ -277,12 +277,13 @@ function createNotificationPager({ load, onNotifications, onPagination, onStatus function createWorkPager({ load, onItems, onPagination, onStatus }) { let pagination = {}; const pending = new Set(); + const completing = new Map(); const labels = { issue: 'issues', pull: 'pull requests', review: 'review requests', }; - return { + const pager = { reset(next) { Object.entries(next || {}).forEach(([stream, value]) => { const current = pagination[stream]; @@ -331,7 +332,20 @@ function createWorkPager({ load, onItems, onPagination, onStatus }) { pending.delete(stream); } }, + loadAll(stream, getExisting) { + if (completing.has(stream)) return completing.get(stream); + const completion = (async () => { + while (pagination[stream]?.has_more) { + const loaded = await pager.loadMore(stream, getExisting()); + if (!loaded) return false; + } + return true; + })().finally(() => completing.delete(stream)); + completing.set(stream, completion); + return completion; + }, }; + return pager; } function createNotificationReader({ diff --git a/tests/test_my_work.py b/tests/test_my_work.py index 351a571..99c2550 100644 --- a/tests/test_my_work.py +++ b/tests/test_my_work.py @@ -173,6 +173,60 @@ process.stdout.write(JSON.stringify(work.agendaMyWork(items, new Date('2026-08-1 assert json.loads(result.stdout) == ["a/r#2", "a/r#10", "z/r#1"] +def test_agenda_pager_loads_every_issue_page_single_flight_and_retries_failed_page(): + script = f""" +const work = require({json.dumps(str(MY_WORK))}); +const calls = []; +let failPage = 3; +let items = [{{id:1, title:'first'}}]; +let pagination = {{issue:{{page:1,total:4,has_more:true}}}}; +const pager = work.createWorkPager({{ + load: async (stream, page) => {{ + calls.push(page); + await new Promise(resolve => setTimeout(resolve, 5)); + if (page === failPage) throw new Error('offline'); + return {{page,total:4,has_more:page < 4,items:[{{id:page,title:'page '+page}}]}}; + }}, + onItems: (_stream, next) => {{ items = next; }}, + onPagination: next => {{ pagination = next; }}, + onStatus: () => {{}}, +}}); +pager.reset(pagination); +async function run() {{ + const first = pager.loadAll('issue', () => items); + const duplicate = pager.loadAll('issue', () => items); + const failed = await first; + const samePromise = first === duplicate; + failPage = 0; + const retried = await pager.loadAll('issue', () => items); + process.stdout.write(JSON.stringify({{ + failed, samePromise, retried, calls, ids:items.map(item => item.id), pagination, + }})); +}} +run(); +""" + result = subprocess.run(["node", "-e", script], capture_output=True, text=True) + assert result.returncode == 0, result.stderr + assert json.loads(result.stdout) == { + "failed": False, + "samePromise": True, + "retried": True, + "calls": [2, 3, 3, 4], + "ids": [1, 2, 3, 4], + "pagination": {"issue": {"page": 4, "total": 4, "has_more": False}}, + } + + +@pytest.mark.anyio +async def test_agenda_activation_checks_all_issue_pages_before_showing_empty_state(): + html = await dashboard() + + assert "workPager.loadAll('issue'" in html + assert "Checking all assigned deadlines…" in html + assert "Agenda check paused. Retry to check older assigned deadlines." in html + assert "selectedWorkFilter === 'agenda' && workPagination.issue?.has_more" in html + + @pytest.mark.anyio async def test_mobile_queue_finder_is_labeled_thumb_safe_and_offers_older_search(): html = await dashboard()