stackchain-dashboard/frontend/mobile-queue-launcher.js
timmy dbabd8640b
All checks were successful
CI / lint (pull_request) Successful in 3m53s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 7m39s
CI / release-candidate (pull_request) Has been skipped
feat: personalize adaptive mobile queue priority (Closes #1460)
2026-08-27 07:09:06 +00:00

145 lines
5.7 KiB
JavaScript

(function (root, factory) {
if (typeof module === 'object' && module.exports) module.exports = factory;
else root.createMobileQueueLauncher = factory;
})(typeof self !== 'undefined' ? self : this, function createMobileQueueLauncher(options) {
const emptyMessages = {
attention: 'No items need attention.',
update: 'No unread updates are ready to open.',
later: 'No deferred work is ready to open.',
draft: 'No drafts are ready to open.',
};
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 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 = 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'};
const [name, label] = match;
const count = Math.max(0, Number(counts[name]) || 0);
return {name, count, label: label + ' (' + count + ')'};
}
function adaptiveRecommendation() {
const preparation = options.getPreparation ? options.getPreparation() : null;
if (preparation && (preparation.active || Number(preparation.total) > 0)) {
const prefix = preparation.active ? 'Resume preparation · ' : 'Start day · ';
return {name: 'prepare', count: Math.max(0, Number(preparation.total) || 0), label: prefix + preparation.label};
}
return recommend();
}
function presentation() {
const counts = options.getCounts ? options.getCounts() : {};
const names = activeQueueNames();
const active = names
.map(name => ({name, count: Math.max(0, Number(counts[name]) || 0)}))
.filter(item => item.count > 0);
names.forEach(name => {
if (counts[name + 'Unavailable'] && !active.some(item => item.name === name)) {
active.push({name, unavailable: true});
}
});
return {
nextUp: adaptiveRecommendation(),
active,
planning: ['today', 'tomorrow', 'week'],
all: allQueues.slice(),
};
}
function renderPresentation() {
const view = presentation();
const planning = new Set(view.planning);
const active = view.active.filter(item => !planning.has(item.name));
const activeNames = new Set(active.map(item => item.name));
if (options.nextAction) {
options.nextAction.textContent = view.nextUp.label;
options.nextAction.dataset.queue = view.nextUp.name;
const prefix = view.nextUp.name === 'prepare' ? 'Start or continue: ' : 'Next up: ';
options.nextAction.setAttribute('aria-label', prefix + view.nextUp.label);
}
active.forEach(item => {
const row = options.rows?.[item.name];
if (!row) return;
if (item.unavailable) row.setAttribute('data-unavailable', 'true');
else row.removeAttribute('data-unavailable');
options.activeList?.append(row);
});
view.planning.forEach(name => {
const row = options.rows?.[name];
if (row) options.planningList?.append(row);
});
view.all.forEach(name => {
if (planning.has(name) || activeNames.has(name)) return;
const row = options.rows?.[name];
if (!row) return;
row.removeAttribute('data-unavailable');
options.allList?.append(row);
});
if (options.activeSection) options.activeSection.hidden = active.length === 0;
return view;
}
function open(name) {
if (name === 'delivery' && options.openDelivery) return options.openDelivery();
if (name === 'gate' && options.openHumanGates) return options.openHumanGates();
if (name === 'today') return options.openToday();
if (name === 'agenda') return options.openAgenda();
if (name === 'update' && options.openUpdates) return options.openUpdates();
if (name === 'following' && options.openFollowing) return options.openFollowing();
if (name === 'filed' && options.openFiled) return options.openFiled();
options.selectFilter(name);
const action = options.firstAction(name);
if (!action) {
options.announce(emptyMessages[name] || 'No work is ready to open.');
return 'empty';
}
action.click();
return 'opened';
}
function continueWork() {
const next = adaptiveRecommendation();
if (next.name === 'prepare') {
options.openPreparation();
return 'prepare';
}
if (next.name === 'find') {
options.openFindWork();
return 'find';
}
return open(next.name);
}
return { open, recommend, adaptiveRecommendation, presentation, renderPresentation, continueWork };
});