Merge pull request 'Complete unread discovery before mobile Updates triage' (#750) from timmy/749-complete-updates-discovery into main
All checks were successful
CI / lint (push) Successful in 1m32s
CI / build-release (push) Successful in 5s
CI / release-candidate (push) Successful in 6s

This commit is contained in:
timmy 2026-08-13 15:34:15 +00:00
commit b93e63ab70
8 changed files with 167 additions and 4 deletions

View File

@ -992,9 +992,19 @@
},
});
const updateTriageLauncher = createUpdateTriageLauncher({
selectUpdates: () => selectMobileQueue('update'),
discover: () => notificationPager.loadAll(() => lastNotifications),
isUpdatesSelected: () => selectedWorkFilter === 'update',
hasMore: () => Boolean(notificationPagination.has_more),
hasCheckpoint: () => updateTriage.resumable(),
resume: () => updateTriage.resume(),
start: () => updateTriage.start(),
announce: message => { qs('#my-work-action-status').textContent = message; },
});
function openUpdateTriage() {
selectMobileQueue('update');
return updateTriage.resumable() ? updateTriage.resume() : updateTriage.start();
return updateTriageLauncher.open();
}
function routedWorkItem(item) {

View File

@ -1073,6 +1073,7 @@
<script src="static/mobile-work-entry.js"></script>
<script src="static/mobile-queue-launcher.js"></script>
<script src="static/update-triage-session.js"></script>
<script src="static/update-triage-launcher.js"></script>
<script src="static/agenda-session-launcher.js"></script>
<script src="static/mobile-launch.js"></script>
<script src="static/mobile-app-shortcuts.js"></script>

View File

@ -248,7 +248,8 @@ function createNotificationSelection({ limit = 50, onChange = () => {} } = {}) {
function createNotificationPager({ load, onNotifications, onPagination, onStatus }) {
let pagination = { page: 1, total: 0, has_more: false };
let pending = false;
return {
let completing = null;
const pager = {
reset(next) {
pagination = { ...pagination, ...(next || {}) };
onPagination(pagination);
@ -282,7 +283,19 @@ function createNotificationPager({ load, onNotifications, onPagination, onStatus
pending = false;
}
},
loadAll(getExisting) {
if (completing) return completing;
completing = (async () => {
while (pagination.has_more) {
const loaded = await pager.loadMore(getExisting());
if (!loaded) return false;
}
return true;
})().finally(() => { completing = null; });
return completing;
},
};
return pager;
}
function createWorkPager({ load, onItems, onPagination, onStatus }) {

View File

@ -77,6 +77,7 @@ const SHELL = [
BASE + 'static/mobile-work-entry.js',
BASE + 'static/mobile-queue-launcher.js',
BASE + 'static/update-triage-session.js',
BASE + 'static/update-triage-launcher.js',
BASE + 'static/agenda-session-launcher.js',
BASE + 'static/mobile-launch.js',
BASE + 'static/mobile-app-shortcuts.js',

View File

@ -0,0 +1,34 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) module.exports = factory;
else root.createUpdateTriageLauncher = factory;
})(typeof self !== 'undefined' ? self : this, function createUpdateTriageLauncher(options) {
let pending = null;
function open() {
if (pending) return pending;
options.selectUpdates();
options.announce('Checking all unread updates…');
pending = Promise.resolve()
.then(() => options.discover())
.then(complete => {
if (!options.isUpdatesSelected()) return 'cancelled';
if (complete === false || options.hasMore()) {
options.announce('Updates check paused. Retry to check older unread updates.');
return 'incomplete';
}
const opened = options.hasCheckpoint() ? options.resume() : options.start();
if (!opened) options.announce('No unread updates are ready.');
return opened ? 'opened' : 'empty';
})
.catch(() => {
if (options.isUpdatesSelected()) {
options.announce('Updates check paused. Retry to check older unread updates.');
}
return 'incomplete';
})
.finally(() => { pending = null; });
return pending;
}
return {open};
});

View File

@ -28,7 +28,7 @@ FEATURE_SOURCES = {
"device-setup": ("static/install-app.js", "static/mobile-device-setup.js"),
"security-center": ("static/security-center.js",),
"today-timer": (
"static/my-work.js", "static/protect-today.js", "static/mobile-task-dock.js", "static/mobile-queue-launcher.js", "static/update-triage-session.js", "static/notification-undo.js", "static/today-timer.js", "static/today-recap.js",
"static/my-work.js", "static/protect-today.js", "static/mobile-task-dock.js", "static/mobile-queue-launcher.js", "static/update-triage-session.js", "static/update-triage-launcher.js", "static/notification-undo.js", "static/today-timer.js", "static/today-recap.js",
"static/today-rollover.js", "static/later-work.js", "static/drafts.js", "static/unfiled-captures.js",
"static/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js",
"static/today-work.js", "static/pick-work.js", "static/batch-find-work.js",

View File

@ -41,6 +41,109 @@ PICK_WORK = Path(__file__).parents[1] / "frontend" / "pick-work.js"
WORK_ROUTE = Path(__file__).parents[1] / "frontend" / "work-route.js"
UPDATE_OWNERSHIP = Path(__file__).parents[1] / "frontend" / "update-ownership.js"
AGENDA_SESSION_LAUNCHER = Path(__file__).parents[1] / "frontend" / "agenda-session-launcher.js"
UPDATE_TRIAGE_LAUNCHER = Path(__file__).parents[1] / "frontend" / "update-triage-launcher.js"
def test_update_triage_launch_waits_for_complete_single_flight_discovery():
script = f"""
const createLauncher = require({json.dumps(str(UPDATE_TRIAGE_LAUNCHER))});
let release;
let discoveries = 0;
let opens = 0;
const messages = [];
const launcher = createLauncher({{
selectUpdates: () => {{}},
discover: () => {{ discoveries += 1; return new Promise(resolve => {{ release = resolve; }}); }},
isUpdatesSelected: () => true,
hasMore: () => false,
hasCheckpoint: () => false,
resume: () => {{ throw new Error('must start'); }},
start: () => {{ opens += 1; return true; }},
announce: message => messages.push(message),
}});
const first = launcher.open();
const second = launcher.open();
Promise.resolve().then(() => {{
const before = {{discoveries, opens, same:first === second}};
release(true);
return Promise.all([first, second]).then(results =>
process.stdout.write(JSON.stringify({{before, discoveries, opens, results, messages}}))
);
}});
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"before": {"discoveries": 1, "opens": 0, "same": True},
"discoveries": 1,
"opens": 1,
"results": ["opened", "opened"],
"messages": ["Checking all unread updates…"],
}
def test_update_triage_launch_preserves_checkpoint_when_discovery_fails():
script = f"""
const createLauncher = require({json.dumps(str(UPDATE_TRIAGE_LAUNCHER))});
const messages = [];
let resumed = 0;
const launcher = createLauncher({{
selectUpdates: () => {{}},
discover: async () => false,
isUpdatesSelected: () => true,
hasMore: () => true,
hasCheckpoint: () => true,
resume: () => {{ resumed += 1; return true; }},
start: () => true,
announce: message => messages.push(message),
}});
launcher.open().then(result => process.stdout.write(JSON.stringify({{result, resumed, messages}})));
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"result": "incomplete",
"resumed": 0,
"messages": [
"Checking all unread updates…",
"Updates check paused. Retry to check older unread updates.",
],
}
def test_notification_pager_load_all_retries_from_failed_page_without_duplicates():
script = f"""
const buildMyWork = require({json.dumps(str(MY_WORK))});
let items = [{{id:1}}];
let fail = true;
const requested = [];
const pager = buildMyWork.createNotificationPager({{
load: async page => {{
requested.push(page);
if (page === 2 && fail) {{ fail = false; throw new Error('offline'); }}
return page === 2 ? {{page:2,total:101,has_more:true,items:[{{id:1}},{{id:51}}]}} :
{{page:3,total:101,has_more:false,items:[{{id:101}}]}};
}},
onNotifications: value => {{ items = value; }},
onPagination: () => {{}},
onStatus: () => {{}},
}});
pager.reset({{page:1,total:101,has_more:true}});
pager.loadAll(() => items).then(firstResult => pager.loadAll(() => items).then(secondResult =>
process.stdout.write(JSON.stringify({{firstResult, secondResult, requested, ids:items.map(item => item.id)}}))
));
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"firstResult": False,
"secondResult": True,
"requested": [2, 2, 3],
"ids": [1, 51, 101],
}
def test_agenda_session_launch_waits_for_complete_single_flight_discovery():

View File

@ -847,6 +847,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
"/dashboard/static/mobile-work-entry.js",
"/dashboard/static/mobile-queue-launcher.js",
"/dashboard/static/update-triage-session.js",
"/dashboard/static/update-triage-launcher.js",
"/dashboard/static/agenda-session-launcher.js",
"/dashboard/static/mobile-launch.js",
"/dashboard/static/mobile-app-shortcuts.js",