diff --git a/frontend/dashboard.css b/frontend/dashboard.css
index 6d2b9d5..90ca0c9 100644
--- a/frontend/dashboard.css
+++ b/frontend/dashboard.css
@@ -1515,6 +1515,16 @@ textarea { resize: vertical; min-height: 120px; }
.mobile-queue-next { display:grid; gap:6px; margin-top:12px; padding:12px; border:1px solid #60a5fa; border-radius:14px; background:#122f50; }
.mobile-queue-next p, .mobile-queue-group h3 { margin:0; }
.mobile-queue-next button { min-height:48px; width:100%; text-align:center; font-weight:800; }
+ .mobile-queue-priority { margin-top:12px; border:1px solid #31577f; border-radius:12px; background:#0b1b30; }
+ .mobile-queue-priority > summary { min-height:44px; display:flex; align-items:center; padding:0 12px; cursor:pointer; font-weight:700; }
+ .mobile-queue-priority > p { margin:0; padding:0 12px 10px; }
+ .mobile-queue-priority-list { display:grid; gap:6px; padding:0 8px; }
+ .mobile-queue-priority-row { display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:center; gap:8px; min-width:0; padding:6px 4px; border-top:1px solid #233f61; }
+ .mobile-queue-priority-row > span:first-child { min-width:0; overflow-wrap:anywhere; font-weight:700; }
+ .mobile-queue-priority-controls { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:4px; }
+ .mobile-queue-priority-controls button { min-height:44px; min-width:64px; padding:6px 8px; }
+ .mobile-queue-priority-footer { display:grid; gap:6px; padding:10px 12px 12px; }
+ .mobile-queue-priority-footer button { min-height:44px; }
.mobile-queue-group { margin-top:16px; }
.mobile-queue-group h3 { font-size:1rem; }
.mobile-queue-all { margin-top:16px; }
diff --git a/frontend/dashboard.js b/frontend/dashboard.js
index 77b83d7..219844b 100644
--- a/frontend/dashboard.js
+++ b/frontend/dashboard.js
@@ -114,6 +114,7 @@
let preparationItems = {};
let offlineWorkMode = false;
let renderMobileQueuePresentation = () => {};
+ let mobileQueuePriority = null;
const followingQueue = attachFollowing(item => {
searchPreviewReturnKind = 'following';
return searchPreview.open(item);
@@ -183,6 +184,7 @@
announce: announceWork,
getCounts: () => queueCounts,
isOnline: () => !offlineWorkMode,
+ getRoutineOrder: () => mobileQueuePriority?.getOrder(),
getPreparation: () => {
const briefing = mobileStartDay.briefing();
return {...briefing, active:mobileStartDay.state().active};
@@ -428,6 +430,20 @@
let planningOwnerLogin = '';
let planningOwnerAccountKey = '';
let activeFlushLogin = '';
+ mobileQueuePriority = createMobileQueuePriority({
+ storage: localStorage,
+ getLogin: () => confirmedOwnerLogin,
+ document,
+ list: qs('#mobile-queue-priority-list'),
+ resetButton: qs('#reset-mobile-queue-priority'),
+ status: qs('#mobile-queue-priority-status'),
+ labels: {
+ attention:'Attention', today:'Today', update:'Updates', agenda:'Agenda',
+ following:'Following', authored:'My PRs', filed:'Filed', later:'Later', draft:'Drafts',
+ },
+ onChange: () => renderMobileQueuePresentation(),
+ });
+ mobileQueuePriority.start();
let rR = null;
function rRC() {
if (rR) return rR;
@@ -5620,6 +5636,8 @@
activeFlushLogin = contextIdentityFresh ? String(snapshot.context.user?.login || '').trim() : '';
if (activeFlushLogin) {
confirmedOwnerLogin = activeFlushLogin;
+ mobileQueuePriority.render();
+ renderMobileQueuePresentation();
void refreshPhotoDraftInbox();
timerView.restore(todaySync.flush());
restoreReleaseReceipt();
@@ -7951,6 +7969,8 @@
if (!saved) return false;
const outage = mode === 'outage';
confirmedOwnerLogin = String(saved.user?.login || '').trim();
+ mobileQueuePriority.render();
+ renderMobileQueuePresentation();
restoreReleaseReceipt();
planningOwnerLogin = confirmedOwnerLogin;
planningOwnerAccountKey = confirmedOwnerLogin && saved.user?.id ?
diff --git a/frontend/index.html b/frontend/index.html
index e490046..5b64cb2 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -2183,6 +2183,15 @@
Start / Continue
+
+ Customize routine order
+ Delivery and Human Gates always stay first. Move the routine queues to match how you work.
+
+
+
Active now
@@ -2402,6 +2411,7 @@
+
diff --git a/frontend/mobile-queue-launcher.js b/frontend/mobile-queue-launcher.js
index 1bfad5a..bbce6f2 100644
--- a/frontend/mobile-queue-launcher.js
+++ b/frontend/mobile-queue-launcher.js
@@ -8,30 +8,38 @@
later: 'No deferred work is ready to open.',
draft: 'No drafts are ready to open.',
};
- const continuation = [
- ['delivery', 'Recover Delivery'],
- ['gate', 'Review Human Gates'],
- ['attention', 'Start Attention'],
- ['today', 'Continue Today'],
- ['update', 'Resume Updates'],
- ['agenda', 'Open Agenda'],
- ['following', 'Review Following'],
- ['authored', 'Open My PRs'],
- ['filed', 'Review Filed'],
- ['later', 'Start Later'],
- ['draft', 'Open Drafts'],
+ const labels = {
+ delivery: 'Recover Delivery', gate: 'Review Human Gates', attention: 'Start Attention',
+ today: 'Continue Today', update: 'Resume Updates', agenda: 'Open Agenda',
+ following: 'Review Following', authored: 'Open My PRs', filed: 'Review Filed',
+ later: 'Start Later', draft: 'Open Drafts',
+ };
+ const criticalQueueNames = ['delivery', 'gate'];
+ const defaultRoutineOrder = [
+ 'attention', 'today', 'update', 'agenda', 'following', 'authored', 'filed', 'later', 'draft',
];
- const activeQueueNames = continuation.map(([name]) => name);
const onlineOnlyQueues = new Set(['delivery', 'gate']);
const allQueues = [
'today', 'tomorrow', 'week', 'agenda', 'delivery', 'gate', 'attention',
'update', 'following', 'filed', 'authored', 'later', 'draft', 'find', 'recaps',
];
+ function routineOrder() {
+ const proposed = options.getRoutineOrder ? options.getRoutineOrder() : defaultRoutineOrder;
+ if (!Array.isArray(proposed) || proposed.length !== defaultRoutineOrder.length ||
+ new Set(proposed).size !== defaultRoutineOrder.length ||
+ proposed.some(name => !defaultRoutineOrder.includes(name))) return defaultRoutineOrder.slice();
+ return proposed.slice();
+ }
+
+ function activeQueueNames() {
+ return criticalQueueNames.concat(routineOrder());
+ }
+
function recommend() {
const counts = options.getCounts ? options.getCounts() : {};
const online = options.isOnline ? options.isOnline() : true;
- const match = continuation.find(([name]) =>
+ const match = activeQueueNames().map(name => [name, labels[name]]).find(([name]) =>
Number(counts[name]) > 0 && (online || !onlineOnlyQueues.has(name))
);
if (!match) return {name: 'find', count: 0, label: 'Find Work'};
@@ -51,10 +59,11 @@
function presentation() {
const counts = options.getCounts ? options.getCounts() : {};
- const active = activeQueueNames
+ const names = activeQueueNames();
+ const active = names
.map(name => ({name, count: Math.max(0, Number(counts[name]) || 0)}))
.filter(item => item.count > 0);
- activeQueueNames.forEach(name => {
+ names.forEach(name => {
if (counts[name + 'Unavailable'] && !active.some(item => item.name === name)) {
active.push({name, unavailable: true});
}
diff --git a/frontend/mobile-queue-priority.js b/frontend/mobile-queue-priority.js
new file mode 100644
index 0000000..702e437
--- /dev/null
+++ b/frontend/mobile-queue-priority.js
@@ -0,0 +1,123 @@
+(function (root, factory) {
+ if (typeof module === 'object' && module.exports) module.exports = factory;
+ else root.createMobileQueuePriority = factory;
+})(typeof self !== 'undefined' ? self : this, function createMobileQueuePriority(options = {}) {
+ const DEFAULT_ORDER = [
+ 'attention', 'today', 'update', 'agenda', 'following', 'authored', 'filed', 'later', 'draft',
+ ];
+ const storage = options.storage;
+ const getLogin = options.getLogin || (() => '');
+ const prefix = 'stackchain-mobile-queue-priority-v1:';
+ const labels = options.labels || {};
+ const documentRef = options.document || (typeof document !== 'undefined' ? document : null);
+
+ function key() {
+ const login = String(getLogin() || '').trim().toLowerCase();
+ return login ? prefix + encodeURIComponent(login) : '';
+ }
+
+ function valid(order) {
+ return Array.isArray(order) && order.length === DEFAULT_ORDER.length &&
+ new Set(order).size === DEFAULT_ORDER.length &&
+ order.every((name, index) => DEFAULT_ORDER.includes(name) && typeof name === 'string');
+ }
+
+ function getOrder() {
+ const accountKey = key();
+ if (!accountKey || !storage) return DEFAULT_ORDER.slice();
+ try {
+ const saved = JSON.parse(storage.getItem(accountKey) || 'null');
+ return valid(saved) ? saved.slice() : DEFAULT_ORDER.slice();
+ } catch (_error) {
+ return DEFAULT_ORDER.slice();
+ }
+ }
+
+ function save(order) {
+ const accountKey = key();
+ if (!accountKey || !storage || !valid(order)) return false;
+ try {
+ storage.setItem(accountKey, JSON.stringify(order));
+ options.onChange?.(order.slice());
+ return true;
+ } catch (_error) {
+ return false;
+ }
+ }
+
+ function move(name, delta) {
+ const order = getOrder();
+ const index = order.indexOf(name);
+ const next = index + (Number(delta) < 0 ? -1 : 1);
+ if (index < 0 || next < 0 || next >= order.length) return order;
+ [order[index], order[next]] = [order[next], order[index]];
+ save(order);
+ return order.slice();
+ }
+
+ function reset() {
+ const accountKey = key();
+ if (accountKey && storage) {
+ try { storage.removeItem(accountKey); } catch (_error) {}
+ }
+ const order = DEFAULT_ORDER.slice();
+ options.onChange?.(order.slice());
+ return order;
+ }
+
+ function displayName(name) {
+ return labels[name] || name.charAt(0).toUpperCase() + name.slice(1);
+ }
+
+ function render() {
+ if (!options.list || !documentRef) return getOrder();
+ const order = getOrder();
+ const signedIn = Boolean(key());
+ const rows = order.map((name, index) => {
+ const row = documentRef.createElement('div');
+ row.setAttribute('data-queue-priority', name);
+ row.setAttribute('class', 'mobile-queue-priority-row');
+ const label = documentRef.createElement('span');
+ label.textContent = displayName(name);
+ const controls = documentRef.createElement('span');
+ controls.setAttribute('class', 'mobile-queue-priority-controls');
+ const earlier = documentRef.createElement('button');
+ earlier.textContent = 'Earlier';
+ earlier.setAttribute('type', 'button');
+ earlier.setAttribute('aria-label', 'Move ' + displayName(name) + ' earlier');
+ earlier.disabled = !signedIn || index === 0;
+ earlier.addEventListener('click', () => {
+ move(name, -1);
+ if (options.status) options.status.textContent = displayName(name) + ' moved earlier.';
+ render();
+ });
+ const later = documentRef.createElement('button');
+ later.textContent = 'Later';
+ later.setAttribute('type', 'button');
+ later.setAttribute('aria-label', 'Move ' + displayName(name) + ' later');
+ later.disabled = !signedIn || index === order.length - 1;
+ later.addEventListener('click', () => {
+ move(name, 1);
+ if (options.status) options.status.textContent = displayName(name) + ' moved later.';
+ render();
+ });
+ controls.append(earlier, later);
+ row.append(label, controls);
+ return row;
+ });
+ options.list.replaceChildren(...rows);
+ if (options.resetButton) options.resetButton.disabled = !signedIn;
+ return order;
+ }
+
+ function start() {
+ options.resetButton?.addEventListener('click', () => {
+ reset();
+ if (options.status) options.status.textContent = 'Routine order reset.';
+ render();
+ });
+ return render();
+ }
+
+ return {getOrder, move, reset, render, start, defaultOrder: () => DEFAULT_ORDER.slice()};
+});
diff --git a/frontend/service-worker.js b/frontend/service-worker.js
index e014eec..873b1ae 100644
--- a/frontend/service-worker.js
+++ b/frontend/service-worker.js
@@ -270,6 +270,7 @@ const SHELL = [
BASE + 'static/mobile-task-dock.js',
BASE + 'static/mobile-first-task.js',
BASE + 'static/mobile-work-entry.js',
+ BASE + 'static/mobile-queue-priority.js',
BASE + 'static/mobile-queue-launcher.js',
BASE + 'static/mobile-delivery-recovery.js',
BASE + 'static/mobile-start-day.js',
diff --git a/src/frontend_bundle.py b/src/frontend_bundle.py
index 6fb5de6..07d382c 100644
--- a/src/frontend_bundle.py
+++ b/src/frontend_bundle.py
@@ -44,7 +44,7 @@ FEATURE_SOURCES = {
),
"today-timer": (
"static/mobile-app-badge.js", "static/conversation.js", "static/widgets.js", "static/voice-transcript-store.js", "static/voice-conversation-capture.js", "static/mobile-launch.js", "static/mobile-insights.js", "static/mobile-app-shortcuts.js", "static/mobile-find-work-nav.js", "static/mobile-pull-refresh.js", "static/live-data-status.js", "static/mobile-search-modal.js", "static/mobile-composer-viewport.js",
- "static/today-completion.js", "static/card-planning.js", "static/work-detail-position.js", "static/work-route.js", "static/commands.js", "static/saved-searches.js", "static/task-overlay-history.js", "static/mobile-search-preview-nav.js", "static/search-reply-draft-store.js", "static/conversation-reply-draft-store.js", "static/conversation-photo-drafts.js", "static/search-defer.js", "static/mobile-search-viewport.js", "static/agenda-replan.js", "static/agenda-calendar.js", "static/protect-today.js", "static/mobile-today-command-bar.js", "static/mobile-task-dock.js", "static/mobile-first-task.js", "static/mobile-work-entry.js", "static/mobile-queue-launcher.js", "static/mobile-delivery-recovery.js", "static/mobile-start-day.js", "static/update-triage-session.js", "static/update-review-handoff.js", "static/update-triage-launcher.js", "static/update-triage-gesture.js", "static/notification-undo.js", "static/today-timer.js", "static/today-break.js", "static/today-progress.js", "static/today-lock-screen.js", "static/today-session-sync.js", "static/today-recap.js", "static/today-wrap-up.js", "static/today-summary.js", "static/today-handoff.js",
+ "static/today-completion.js", "static/card-planning.js", "static/work-detail-position.js", "static/work-route.js", "static/commands.js", "static/saved-searches.js", "static/task-overlay-history.js", "static/mobile-search-preview-nav.js", "static/search-reply-draft-store.js", "static/conversation-reply-draft-store.js", "static/conversation-photo-drafts.js", "static/search-defer.js", "static/mobile-search-viewport.js", "static/agenda-replan.js", "static/agenda-calendar.js", "static/protect-today.js", "static/mobile-today-command-bar.js", "static/mobile-task-dock.js", "static/mobile-first-task.js", "static/mobile-work-entry.js", "static/mobile-queue-priority.js", "static/mobile-queue-launcher.js", "static/mobile-delivery-recovery.js", "static/mobile-start-day.js", "static/update-triage-session.js", "static/update-review-handoff.js", "static/update-triage-launcher.js", "static/update-triage-gesture.js", "static/notification-undo.js", "static/today-timer.js", "static/today-break.js", "static/today-progress.js", "static/today-lock-screen.js", "static/today-session-sync.js", "static/today-recap.js", "static/today-wrap-up.js", "static/today-summary.js", "static/today-handoff.js",
"static/later-work.js", "static/detail-defer.js", "static/later-picker.js", "static/drafts.js", "static/photo-draft-inbox.js", "static/unfiled-draft-sync.js",
"static/assign-and-start.js", "static/filed-claim.js", "static/queue-today.js", "static/create-and-start.js",
"static/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js",
diff --git a/tests/e2e/test_adaptive_mobile_queues_release.py b/tests/e2e/test_adaptive_mobile_queues_release.py
index 867a95f..6534a2d 100644
--- a/tests/e2e/test_adaptive_mobile_queues_release.py
+++ b/tests/e2e/test_adaptive_mobile_queues_release.py
@@ -166,3 +166,66 @@ def test_adaptive_queues_keep_start_continue_actionable_offline_on_phone():
assert bounds["y"] + bounds["height"] <= viewport["height"]
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
browser.close()
+
+
+@pytest.mark.parametrize("viewport", [
+ {"width": 320, "height": 568},
+ {"width": 375, "height": 667},
+ {"width": 430, "height": 932},
+])
+def test_operator_reorders_routine_queues_without_leaking_priority_between_accounts(viewport):
+ with sync_playwright() as playwright:
+ browser = playwright.chromium.launch(headless=True)
+ page = browser.new_page(viewport=viewport)
+ page.set_content((FRONTEND / "index.html").read_text())
+ page.add_style_tag(path=FRONTEND / "dashboard.css")
+ page.add_script_tag(path=FRONTEND / "mobile-queue-priority.js")
+ page.add_script_tag(path=FRONTEND / "mobile-queue-launcher.js")
+ page.evaluate("""() => {
+ const values = new Map();
+ window.login = 'alice';
+ const rows = Object.fromEntries(Array.from(document.querySelectorAll('[data-mobile-queue]'))
+ .map(row => [row.dataset.mobileQueue, row]));
+ window.launcher = createMobileQueueLauncher({
+ getCounts:() => ({attention:2, following:3, authored:1}),
+ getRoutineOrder:() => window.priority.getOrder(), rows,
+ nextAction:document.querySelector('#mobile-queue-next-action'),
+ activeList:document.querySelector('#mobile-queue-active-list'),
+ planningList:document.querySelector('#mobile-queue-planning-list'),
+ allList:document.querySelector('#mobile-queue-all-list'),
+ activeSection:document.querySelector('#mobile-queue-active-list').parentElement,
+ });
+ window.priority = createMobileQueuePriority({
+ storage:{getItem:key => values.get(key) || null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)},
+ getLogin:() => window.login, document,
+ list:document.querySelector('#mobile-queue-priority-list'),
+ resetButton:document.querySelector('#reset-mobile-queue-priority'),
+ status:document.querySelector('#mobile-queue-priority-status'),
+ labels:{attention:'Attention',today:'Today',update:'Updates',agenda:'Agenda',following:'Following',authored:'My PRs',filed:'Filed',later:'Later',draft:'Drafts'},
+ onChange:() => window.launcher.renderPresentation(),
+ });
+ window.priority.start(); window.launcher.renderPresentation();
+ document.querySelector('#mobile-queue-priority').open = true;
+ document.querySelector('#mobile-queue-sheet').showModal();
+ }""")
+
+ for _ in range(4):
+ page.get_by_role("button", name="Move Following earlier").click()
+ expect(page.locator("#mobile-queue-next-action")).to_have_text("Review Following (3)")
+ assert page.locator("#mobile-queue-active-list [data-mobile-queue]").evaluate_all(
+ "rows => rows.map(row => row.dataset.mobileQueue)"
+ ) == ["following", "attention", "authored"]
+ expect(page.locator("#mobile-queue-priority-status")).to_have_text("Following moved earlier.")
+ controls = page.locator(".mobile-queue-priority-controls button")
+ assert controls.count() == 18
+ assert all((controls.nth(i).bounding_box() or {}).get("height", 0) >= 44 for i in range(controls.count()))
+ assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
+
+ page.evaluate("""() => {
+ window.login = 'bob'; window.priority.render(); window.launcher.renderPresentation();
+ }""")
+ expect(page.locator("#mobile-queue-next-action")).to_have_text("Start Attention (2)")
+ assert page.locator("#mobile-queue-priority-list [data-queue-priority]").evaluate_all(
+ "rows => rows.map(row => row.dataset.queuePriority)"
+ )[:3] == ["attention", "today", "update"]
+ browser.close()
diff --git a/tests/test_mobile_task_dock.py b/tests/test_mobile_task_dock.py
index 32f426f..b88d62f 100644
--- a/tests/test_mobile_task_dock.py
+++ b/tests/test_mobile_task_dock.py
@@ -10,6 +10,7 @@ from tests.dashboard_bundle import dashboard
DOCK = Path(__file__).resolve().parents[1] / "frontend" / "mobile-task-dock.js"
ENTRY = Path(__file__).resolve().parents[1] / "frontend" / "mobile-work-entry.js"
QUEUE_LAUNCHER = Path(__file__).resolve().parents[1] / "frontend" / "mobile-queue-launcher.js"
+QUEUE_PRIORITY = Path(__file__).resolve().parents[1] / "frontend" / "mobile-queue-priority.js"
TIMER = Path(__file__).resolve().parents[1] / "frontend" / "today-timer.js"
@@ -48,6 +49,88 @@ process.stdout.write(JSON.stringify({{modes, calls}}));
}
+def test_mobile_queue_priority_persists_complete_account_scoped_routine_order():
+ script = f"""
+const createPriority = require({json.dumps(str(QUEUE_PRIORITY))});
+const values = new Map();
+const storage = {{
+ getItem:key => values.has(key) ? values.get(key) : null,
+ setItem:(key, value) => values.set(key, value),
+ removeItem:key => values.delete(key),
+}};
+let login = 'alice';
+const priority = createPriority({{storage, getLogin:() => login}});
+const original = priority.getOrder();
+priority.move('following', -1);
+priority.move('following', -1);
+priority.move('following', -1);
+const alice = priority.getOrder();
+login = 'bob';
+const bob = priority.getOrder();
+login = '';
+const anonymous = priority.getOrder();
+login = 'alice';
+priority.reset();
+process.stdout.write(JSON.stringify({{
+ original, alice, bob, anonymous, reset:priority.getOrder(), keys:Array.from(values.keys()),
+}}));
+"""
+ result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
+
+ assert result.returncode == 0, result.stderr
+ default = ["attention", "today", "update", "agenda", "following", "authored", "filed", "later", "draft"]
+ assert json.loads(result.stdout) == {
+ "original": default,
+ "alice": ["attention", "following", "today", "update", "agenda", "authored", "filed", "later", "draft"],
+ "bob": default,
+ "anonymous": default,
+ "reset": default,
+ "keys": [],
+ }
+
+
+def test_mobile_queue_priority_renders_keyboard_controls_and_updates_immediately():
+ script = f"""
+const createPriority = require({json.dumps(str(QUEUE_PRIORITY))});
+class Element {{
+ constructor(tag='div') {{ this.tag=tag; this.children=[]; this.listeners={{}}; this.attributes={{}}; this.disabled=false; this.textContent=''; }}
+ append(...items) {{ this.children.push(...items); }}
+ replaceChildren(...items) {{ this.children=[...items]; }}
+ addEventListener(name, callback) {{ this.listeners[name]=callback; }}
+ setAttribute(name, value) {{ this.attributes[name]=value; }}
+ click() {{ this.listeners.click?.(); }}
+}}
+const values = new Map();
+const list = new Element(); const resetButton = new Element('button'); const status = new Element();
+const priority = createPriority({{
+ storage:{{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}},
+ getLogin:()=>'alice', document:{{createElement:tag=>new Element(tag)}}, list, resetButton, status,
+ labels:{{attention:'Attention',today:'Today',update:'Updates',agenda:'Agenda',following:'Following',authored:'My PRs',filed:'Filed',later:'Later',draft:'Drafts'}},
+}});
+priority.start();
+for (let index=0; index<3; index += 1) {{
+ const row = list.children.find(item => item.attributes['data-queue-priority'] === 'following');
+ row.children[1].children[0].click();
+}}
+const following = list.children.find(item => item.attributes['data-queue-priority'] === 'following');
+process.stdout.write(JSON.stringify({{
+ order:list.children.map(item => item.attributes['data-queue-priority']),
+ earlierLabel:following.children[1].children[0].attributes['aria-label'],
+ laterLabel:following.children[1].children[1].attributes['aria-label'],
+ status:status.textContent,
+}}));
+"""
+ result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
+
+ assert result.returncode == 0, result.stderr
+ assert json.loads(result.stdout) == {
+ "order": ["attention", "following", "today", "update", "agenda", "authored", "filed", "later", "draft"],
+ "earlierLabel": "Move Following earlier",
+ "laterLabel": "Move Following later",
+ "status": "Following moved earlier.",
+ }
+
+
def test_mobile_work_entry_preserves_active_today_then_launches_highest_priority_queue():
script = f"""
const createEntry = require({json.dumps(str(ENTRY))});
@@ -300,6 +383,22 @@ async def test_mobile_queue_sheet_prioritizes_next_active_and_planning_without_d
assert html.count(f'