feat: prioritize mobile delivery recovery (Closes #1012)
All checks were successful
CI / lint (pull_request) Successful in 2m58s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 2m5s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy 2026-08-17 08:30:09 +00:00
parent db28cc5690
commit 246f995b56
5 changed files with 58 additions and 11 deletions

View File

@ -1531,6 +1531,7 @@
<div class="mobile-queue-list"> <div class="mobile-queue-list">
<button data-mobile-queue="today" type="button"><span><strong>Today</strong><small>Planned work</small></span><span data-mobile-queue-count="today">0</span></button> <button data-mobile-queue="today" type="button"><span><strong>Today</strong><small>Planned work</small></span><span data-mobile-queue-count="today">0</span></button>
<button data-mobile-queue="agenda" type="button"><span><strong>Agenda</strong><small>Upcoming deadlines</small></span><span data-mobile-queue-count="agenda">0</span></button> <button data-mobile-queue="agenda" type="button"><span><strong>Agenda</strong><small>Upcoming deadlines</small></span><span data-mobile-queue-count="agenda">0</span></button>
<button data-mobile-queue="delivery" type="button"><span><strong>Delivery</strong><small>Needs recovery</small></span><span data-mobile-queue-count="delivery">0</span></button>
<button data-mobile-queue="attention" type="button"><span><strong>Attention</strong><small>Needs a response</small></span><span data-mobile-queue-count="attention">0</span></button> <button data-mobile-queue="attention" type="button"><span><strong>Attention</strong><small>Needs a response</small></span><span data-mobile-queue-count="attention">0</span></button>
<button data-mobile-queue="update" type="button" aria-label="Updates, 0 unread conversations"><span><strong>Updates</strong><small>Unread conversations</small></span><span data-mobile-queue-count="update">0</span></button> <button data-mobile-queue="update" type="button" aria-label="Updates, 0 unread conversations"><span><strong>Updates</strong><small>Unread conversations</small></span><span data-mobile-queue-count="update">0</span></button>
<button data-mobile-queue="filed" type="button"><span><strong>Filed</strong><small>Issues you delegated</small></span><span data-mobile-queue-count="filed">0</span></button> <button data-mobile-queue="filed" type="button"><span><strong>Filed</strong><small>Issues you delegated</small></span><span data-mobile-queue-count="filed">0</span></button>

View File

@ -9,6 +9,7 @@
draft: 'No drafts are ready to open.', draft: 'No drafts are ready to open.',
}; };
const continuation = [ const continuation = [
['delivery', 'Recover Delivery'],
['attention', 'Start Attention'], ['attention', 'Start Attention'],
['today', 'Continue Today'], ['today', 'Continue Today'],
['update', 'Resume Updates'], ['update', 'Resume Updates'],

View File

@ -66,9 +66,9 @@
function updateWork(mode) { function updateWork(mode) {
const text = { const text = {
continue:'Continue', resume:'Resume', start:'Start', plan:'Plan', find:'Find', continue:'Continue', resume:'Resume', start:'Start', plan:'Plan', find:'Find',
attention:'Attention', update:'Updates', agenda:'Agenda', filed:'Filed', later:'Later', draft:'Drafts', delivery:'Delivery', attention:'Attention', update:'Updates', agenda:'Agenda', filed:'Filed', later:'Later', draft:'Drafts',
}[mode] || 'Work'; }[mode] || 'Work';
const queue = ['Attention', 'Updates', 'Agenda', 'Filed', 'Later', 'Drafts'].includes(text); const queue = ['Delivery', 'Attention', 'Updates', 'Agenda', 'Filed', 'Later', 'Drafts'].includes(text);
if (options.workLabel) options.workLabel.textContent = text; if (options.workLabel) options.workLabel.textContent = text;
if (buttons.work) buttons.work.setAttribute('aria-label', queue if (buttons.work) buttons.work.setAttribute('aria-label', queue
? (mode === 'update' ? 'Resume ' : 'Open ') + text ? (mode === 'update' ? 'Resume ' : 'Open ') + text
@ -91,9 +91,9 @@
} }
function updateQueues(counts) { function updateQueues(counts) {
const names = 'today agenda attention update filed later draft'.split(' '); const names = 'today agenda delivery attention update filed later draft'.split(' ');
const normalized = Object.fromEntries(names.map(name => [name, Math.max(0, Number(counts?.[name]) || 0)])); const normalized = Object.fromEntries(names.map(name => [name, Math.max(0, Number(counts?.[name]) || 0)]));
const actionableNames = ['today', 'attention', 'update', 'filed', 'later', 'draft']; const actionableNames = ['today', 'delivery', 'attention', 'update', 'filed', 'later', 'draft'];
const active = actionableNames.reduce((total, name) => total + (normalized[name] > 0 ? 1 : 0), 0); const active = actionableNames.reduce((total, name) => total + (normalized[name] > 0 ? 1 : 0), 0);
Object.entries(options.queueCounts || {}).forEach(([name, element]) => { Object.entries(options.queueCounts || {}).forEach(([name, element]) => {
element.textContent = String(normalized[name] || 0); element.textContent = String(normalized[name] || 0);
@ -117,6 +117,7 @@
? 'Queues: no active queues; no upcoming deadlines' ? 'Queues: no active queues; no upcoming deadlines'
: 'Queues: Today ' + normalized.today : 'Queues: Today ' + normalized.today
+ ', Agenda ' + normalized.agenda + ' due' + ', Agenda ' + normalized.agenda + ' due'
+ ', Delivery ' + normalized.delivery
+ ', Attention ' + normalized.attention + ', Attention ' + normalized.attention
+ ', Updates ' + normalized.update + ', Updates ' + normalized.update
+ ', Filed ' + normalized.filed + ', Filed ' + normalized.filed

View File

@ -101,6 +101,15 @@ def test_release_artifact_bootstraps_mobile_home_and_returns_from_insights(
bounds = control.bounding_box() bounds = control.bounding_box()
assert bounds and bounds["height"] >= 44 assert bounds and bounds["height"] >= 44
page.locator('[data-mobile-task="queues"]').click()
delivery_queue = page.locator('[data-mobile-queue="delivery"]')
expect(delivery_queue).to_be_visible()
expect(delivery_queue).to_contain_text("Delivery")
delivery_bounds = delivery_queue.bounding_box()
assert delivery_bounds and delivery_bounds["height"] >= 44
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
page.locator("#close-mobile-queues").click()
page.locator("#app-menu-toggle").click() page.locator("#app-menu-toggle").click()
page.locator("#open-insights").click() page.locator("#open-insights").click()
expect(page.locator("#insights-sheet")).to_be_visible() expect(page.locator("#insights-sheet")).to_be_visible()

View File

@ -355,7 +355,7 @@ sheet.close = function () {{ this.open = false; this.listeners.close?.(); }};
const close = new FakeElement(); const close = new FakeElement();
const badge = new FakeElement(); const badge = new FakeElement();
const deadline = new FakeElement(); const deadline = new FakeElement();
const rows = Object.fromEntries(['today','agenda','attention','update','filed','later','draft','recaps'].map(name => [name, new FakeElement()])); const rows = Object.fromEntries(['today','agenda','delivery','attention','update','filed','later','draft','recaps'].map(name => [name, new FakeElement()]));
const counts = Object.fromEntries(Object.keys(rows).map(name => [name, new FakeElement()])); const counts = Object.fromEntries(Object.keys(rows).map(name => [name, new FakeElement()]));
const selected = []; const selected = [];
const utilities = []; const utilities = [];
@ -366,7 +366,7 @@ const dock = createDock({{
observe() {{}}, observe() {{}},
}}); }});
dock.start(); dock.start();
dock.updateQueues({{today:2, agenda:5, attention:1, update:5, filed:2, later:3, draft:4}}); dock.updateQueues({{today:2, agenda:5, delivery:1, attention:1, update:5, filed:2, later:3, draft:4}});
queues.click(); queues.click();
const opened = sheet.open; const opened = sheet.open;
rows.update.click(); rows.update.click();
@ -411,9 +411,9 @@ process.stdout.write(JSON.stringify({{
"utilities": ["recaps:trigger"], "utilities": ["recaps:trigger"],
"badge": "0 active", "badge": "0 active",
"populated": { "populated": {
"badge": "6 active", "badge": "7 active",
"badgeHidden": False, "badgeHidden": False,
"badgeLabel": "Queues: Today 2, Agenda 5 due, Attention 1, Updates 5, Filed 2, Later 3, Drafts 4; 6 active queues", "badgeLabel": "Queues: Today 2, Agenda 5 due, Delivery 1, Attention 1, Updates 5, Filed 2, Later 3, Drafts 4; 7 active queues",
"deadline": "5 due", "deadline": "5 due",
"deadlineHidden": False, "deadlineHidden": False,
"agendaDue": "true", "agendaDue": "true",
@ -422,13 +422,13 @@ process.stdout.write(JSON.stringify({{
"badge": "1 active", "badge": "1 active",
"badgeHidden": False, "badgeHidden": False,
"deadlineHidden": True, "deadlineHidden": True,
"badgeLabel": "Queues: Today 0, Agenda 0 due, Attention 0, Updates 7, Filed 0, Later 0, Drafts 0; 1 active queue", "badgeLabel": "Queues: Today 0, Agenda 0 due, Delivery 0, Attention 0, Updates 7, Filed 0, Later 0, Drafts 0; 1 active queue",
}, },
"clearedBadgeHidden": True, "clearedBadgeHidden": True,
"clearedDeadlineHidden": True, "clearedDeadlineHidden": True,
"clearedAgendaDue": None, "clearedAgendaDue": None,
"clearedBadgeLabel": "Queues: no active queues; no upcoming deadlines", "clearedBadgeLabel": "Queues: no active queues; no upcoming deadlines",
"counts": {"today": "0", "agenda": "0", "attention": "0", "update": "0", "filed": "0", "later": "0", "draft": "0", "recaps": "0"}, "counts": {"today": "0", "agenda": "0", "delivery": "0", "attention": "0", "update": "0", "filed": "0", "later": "0", "draft": "0", "recaps": "0"},
"updateLabel": "Updates, 0 unread conversations", "updateLabel": "Updates, 0 unread conversations",
"queueFocuses": 3, "queueFocuses": 3,
"columnState": None, "columnState": None,
@ -503,6 +503,37 @@ process.stdout.write(JSON.stringify({{result, calls}}));
assert json.loads(result.stdout) == {"result": "opened", "calls": ["delivery-recovery"]} assert json.loads(result.stdout) == {"result": "opened", "calls": ["delivery-recovery"]}
def test_mobile_work_prioritizes_and_revalidates_actionable_delivery_recovery():
script = f"""
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
const calls = [];
let counts = {{delivery:2, attention:3, today:1, update:4}};
const launcher = createLauncher({{
getCounts: () => counts,
openDelivery: () => {{ calls.push('delivery-recovery'); return 'opened-delivery'; }},
openUpdates: () => {{ calls.push('updates'); return 'opened-updates'; }},
selectFilter: name => calls.push('filter:' + name),
firstAction: () => null,
announce: () => {{}},
openFindWork: () => calls.push('find'),
}});
const delivery = launcher.recommend();
const opened = launcher.continueWork();
counts = {{delivery:0, attention:0, today:0, update:4}};
const afterRecovery = launcher.recommend();
process.stdout.write(JSON.stringify({{delivery, opened, afterRecovery, calls}}));
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"delivery": {"name": "delivery", "count": 2, "label": "Recover Delivery (2)"},
"opened": "opened-delivery",
"afterRecovery": {"name": "update", "count": 4, "label": "Resume Updates (4)"},
"calls": ["delivery-recovery"],
}
def test_mobile_work_continues_into_filed_follow_up_before_later_work(): def test_mobile_work_continues_into_filed_follow_up_before_later_work():
script = f""" script = f"""
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
@ -600,7 +631,7 @@ const work = {{attributes: {{}}, setAttribute(name, value) {{ this.attributes[na
const workLabel = {{textContent:''}}; const workLabel = {{textContent:''}};
const dock = createDock({{nav:{{}}, buttons:{{work}}, workLabel}}); const dock = createDock({{nav:{{}}, buttons:{{work}}, workLabel}});
const labels = {{}}; const labels = {{}};
for (const mode of ['attention','update','agenda','filed','later','draft']) {{ for (const mode of ['delivery','attention','update','agenda','filed','later','draft']) {{
dock.updateWork(mode); dock.updateWork(mode);
labels[mode] = [workLabel.textContent, work.attributes['aria-label']]; labels[mode] = [workLabel.textContent, work.attributes['aria-label']];
}} }}
@ -610,6 +641,7 @@ process.stdout.write(JSON.stringify(labels));
assert result.returncode == 0, result.stderr assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == { assert json.loads(result.stdout) == {
"delivery": ["Delivery", "Open Delivery"],
"attention": ["Attention", "Open Attention"], "attention": ["Attention", "Open Attention"],
"update": ["Updates", "Resume Updates"], "update": ["Updates", "Resume Updates"],
"agenda": ["Agenda", "Open Agenda"], "agenda": ["Agenda", "Open Agenda"],
@ -646,6 +678,9 @@ async def test_dashboard_renders_and_wires_mobile_queue_switcher():
assert "counts.agenda = agendaItems.length;" in html assert "counts.agenda = agendaItems.length;" in html
assert 'data-agenda-group' in html assert 'data-agenda-group' in html
assert 'data-mobile-queue="attention"' in html assert 'data-mobile-queue="attention"' in html
assert 'data-mobile-queue="delivery"' in html
assert '<strong>Delivery</strong><small>Needs recovery</small>' in html
assert 'data-mobile-queue-count="delivery"' in html
assert 'data-mobile-queue="update"' in html assert 'data-mobile-queue="update"' in html
assert '<strong>Updates</strong><small>Unread conversations</small>' in html assert '<strong>Updates</strong><small>Unread conversations</small>' in html
assert 'data-mobile-queue="filed"' in html assert 'data-mobile-queue="filed"' in html