diff --git a/frontend/index.html b/frontend/index.html
index d16c9c6..16b0332 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1531,6 +1531,7 @@
+
diff --git a/frontend/mobile-queue-launcher.js b/frontend/mobile-queue-launcher.js
index 1a5ef30..5aa1d50 100644
--- a/frontend/mobile-queue-launcher.js
+++ b/frontend/mobile-queue-launcher.js
@@ -9,6 +9,7 @@
draft: 'No drafts are ready to open.',
};
const continuation = [
+ ['delivery', 'Recover Delivery'],
['attention', 'Start Attention'],
['today', 'Continue Today'],
['update', 'Resume Updates'],
diff --git a/frontend/mobile-task-dock.js b/frontend/mobile-task-dock.js
index 49be4ae..a76fdda 100644
--- a/frontend/mobile-task-dock.js
+++ b/frontend/mobile-task-dock.js
@@ -66,9 +66,9 @@
function updateWork(mode) {
const text = {
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';
- 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 (buttons.work) buttons.work.setAttribute('aria-label', queue
? (mode === 'update' ? 'Resume ' : 'Open ') + text
@@ -91,9 +91,9 @@
}
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 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);
Object.entries(options.queueCounts || {}).forEach(([name, element]) => {
element.textContent = String(normalized[name] || 0);
@@ -117,6 +117,7 @@
? 'Queues: no active queues; no upcoming deadlines'
: 'Queues: Today ' + normalized.today
+ ', Agenda ' + normalized.agenda + ' due'
+ + ', Delivery ' + normalized.delivery
+ ', Attention ' + normalized.attention
+ ', Updates ' + normalized.update
+ ', Filed ' + normalized.filed
diff --git a/tests/e2e/test_mobile_home_bootstrap_release.py b/tests/e2e/test_mobile_home_bootstrap_release.py
index 6af1484..ab94839 100644
--- a/tests/e2e/test_mobile_home_bootstrap_release.py
+++ b/tests/e2e/test_mobile_home_bootstrap_release.py
@@ -101,6 +101,15 @@ def test_release_artifact_bootstraps_mobile_home_and_returns_from_insights(
bounds = control.bounding_box()
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("#open-insights").click()
expect(page.locator("#insights-sheet")).to_be_visible()
diff --git a/tests/test_mobile_task_dock.py b/tests/test_mobile_task_dock.py
index fceff82..023ca97 100644
--- a/tests/test_mobile_task_dock.py
+++ b/tests/test_mobile_task_dock.py
@@ -355,7 +355,7 @@ sheet.close = function () {{ this.open = false; this.listeners.close?.(); }};
const close = new FakeElement();
const badge = 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 selected = [];
const utilities = [];
@@ -366,7 +366,7 @@ const dock = createDock({{
observe() {{}},
}});
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();
const opened = sheet.open;
rows.update.click();
@@ -411,9 +411,9 @@ process.stdout.write(JSON.stringify({{
"utilities": ["recaps:trigger"],
"badge": "0 active",
"populated": {
- "badge": "6 active",
+ "badge": "7 active",
"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",
"deadlineHidden": False,
"agendaDue": "true",
@@ -422,13 +422,13 @@ process.stdout.write(JSON.stringify({{
"badge": "1 active",
"badgeHidden": False,
"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,
"clearedDeadlineHidden": True,
"clearedAgendaDue": None,
"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",
"queueFocuses": 3,
"columnState": None,
@@ -503,6 +503,37 @@ process.stdout.write(JSON.stringify({{result, calls}}));
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():
script = f"""
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 dock = createDock({{nav:{{}}, buttons:{{work}}, workLabel}});
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);
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 json.loads(result.stdout) == {
+ "delivery": ["Delivery", "Open Delivery"],
"attention": ["Attention", "Open Attention"],
"update": ["Updates", "Resume Updates"],
"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 'data-agenda-group' in html
assert 'data-mobile-queue="attention"' in html
+ assert 'data-mobile-queue="delivery"' in html
+ assert 'DeliveryNeeds recovery' in html
+ assert 'data-mobile-queue-count="delivery"' in html
assert 'data-mobile-queue="update"' in html
assert 'UpdatesUnread conversations' in html
assert 'data-mobile-queue="filed"' in html