feat: guide blocked deliveries through Prepare Today (Closes #1010)
CI / lint (pull_request) Successful in 2m50s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 1m53s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-17 08:01:48 +00:00
1 parent 1baa9c1f91
commit 26aa12019e
18 files changed
+154 -34

No files matched your search

+15 -2
View File
@@ -91,6 +91,14 @@
qs('#my-work').focus();
}
let mobileQueueCounts = {};
function openDeliveryRecovery() {
selectMobileQueue('draft');
const deliveryCenterTitle = qs('#delivery-center-title');
if (!deliveryCenterTitle) return 'empty';
deliveryCenterTitle.scrollIntoView({block:'start'});
deliveryCenterTitle.focus({preventScroll:true});
return 'opened';
}
function openFiledFollowUp() {
selectMobileQueue('filed');
const target = filedFollowUpTarget(completedFiledReview.visible(lastMyWork));
@@ -104,6 +112,7 @@
return target.kind === 'update' ? 'opened-update' : 'opened-issue';
}
const mobileQueueLauncher = createMobileQueueLauncher({
openDelivery: openDeliveryRecovery,
openToday: () => mobileWorkEntry.open(),
openAgenda: openAgendaSession,
openUpdates: openUpdateTriage,
@@ -3024,6 +3033,7 @@
counts.today = todayMyWork.length;
counts.later = laterMyWork.length;
counts.draft = lastDrafts.length;
counts.delivery = draftInbox.partition(lastDrafts).actionable;
if (!launchFilterResolved) {
selectedWorkFilter = mobileLaunch.chooseFilter({
saved: savedWorkFilter, today: counts.today, attention: counts.attention,
@@ -3039,7 +3049,10 @@
if (element) element.textContent = count;
});
mobileQueueCounts = counts;
mobileStartDay.reconcile({authoritative:authoritativeMyWorkRefresh});
mobileStartDay.reconcile({
authoritative:authoritativeMyWorkRefresh,
authoritativePhases:['delivery'],
});
mobileStartDay.render();
mobileTaskDock.updateQueues(counts);
mobileTaskDock.updateWork(mobileWorkEntry.mode());
@@ -3165,7 +3178,7 @@
'<div class="draft-actions">' + outboxActions + '</div></article>';
};
const deliverySummary = '<section class="delivery-center" aria-labelledby="delivery-center-title">' +
'<div><h3 id="delivery-center-title">Delivery center</h3>' +
'<div><h3 id="delivery-center-title" tabindex="-1">Delivery center</h3>' +
'<p class="small">Waiting <strong data-delivery-count="waiting">' + deliveryCenter.counts.waiting + '</strong> · ' +
'Sending <strong data-delivery-count="sending">' + deliveryCenter.counts.sending + '</strong> · ' +
'Needs attention <strong data-delivery-count="attention">' + deliveryCenter.counts.attention + '</strong> · ' +
+9 -2
View File
@@ -240,7 +240,14 @@ function createDraftInbox({ storage, getCurrentLogin = () => '', now = () => Dat
}
function partition(items = list()) {
const deliveries = items.filter(item => item.kind === 'issue-outbox' || item.kind === 'authored-outbox');
const isDelivery = item => item.kind === 'issue-outbox' || item.kind === 'authored-outbox';
const needsAction = item => isDelivery(item) && (
item.status === 'attention' || item.status === 'authorization' || item.status === 'completion' ||
item.delivery_state === 'uncertain' || item.quarantined
);
const deliveries = items.filter(isDelivery).sort((left, right) =>
Number(needsAction(right)) - Number(needsAction(left))
);
const drafts = items.filter(item => item.kind !== 'issue-outbox' && item.kind !== 'authored-outbox');
const counts = deliveries.reduce((summary, item) => {
const state = item.status === 'sending' ? 'sending' : (item.status === 'attention' ? 'attention' :
@@ -251,7 +258,7 @@ function createDraftInbox({ storage, getCurrentLogin = () => '', now = () => Dat
const retryable = deliveries.filter(item =>
item.status === 'queued' && !item.authorization_required && !item.quarantined && item.delivery_state !== 'uncertain'
);
return { drafts, deliveries, counts, retryable };
return { drafts, deliveries, counts, retryable, actionable:deliveries.filter(needsAction).length };
}
function deliveryLabel(item) {
+1
View File
@@ -28,6 +28,7 @@
}
function open(name) {
if (name === 'delivery' && options.openDelivery) return options.openDelivery();
if (name === 'today') return options.openToday();
if (name === 'agenda') return options.openAgenda();
if (name === 'update' && options.openUpdates) return options.openUpdates();
+11 -4
View File
@@ -5,6 +5,7 @@
const checkpointKey = 'stackchain.mobile-start-day.v1';
const storage = options.storage || (typeof localStorage !== 'undefined' ? localStorage : null);
const reviewOrder = [
['delivery', 'Delivery recovery'],
['agenda', 'Agenda'],
['attention', 'Attention'],
['update', 'Updates'],
@@ -73,13 +74,19 @@
.filter(phase => phase.count > 0);
const total = phases.reduce((sum, phase) => sum + phase.count, 0);
const today = count(counts.today);
const delivery = count(counts.delivery);
const other = total - delivery;
const next = phases.length ? phases[0].name : (today ? 'today' : 'find');
const nextLabel = phases.length ? 'Review ' + phases[0].label : (today ? 'Continue Today' : 'Find Work');
const nextLabel = next === 'delivery' ? 'Review Delivery' :
(phases.length ? 'Review ' + phases[0].label : (today ? 'Continue Today' : 'Find Work'));
return {
total,
next,
label: nextLabel,
summary: total ? total + ' items before Today · ' + today + ' planned' :
summary: delivery ? delivery + (delivery === 1 ? ' delivery needs' : ' deliveries need') +
' action before Today' + (other ? ' · ' + other + ' other ' + (other === 1 ? 'item' : 'items') : '') +
' · ' + today + ' planned' :
total ? total + ' items before Today · ' + today + ' planned' :
(today ? 'Review clear · ' + today + ' planned' : 'Review clear · Today is empty'),
phases,
};
@@ -107,9 +114,9 @@
return true;
}
function reconcile({authoritative = false} = {}) {
function reconcile({authoritative = false, authoritativePhases = []} = {}) {
const saved = checkpoint();
if (!authoritative || !saved?.phase) return false;
if (!saved?.phase || (!authoritative && !authoritativePhases.includes(saved.phase))) return false;
const counts = options.getCounts ? options.getCounts() : {};
if (count(counts[saved.phase]) > 0) return false;
return completePhase(saved.phase);
+1 -1
View File
@@ -1,7 +1,7 @@
const BASE = new URL('./', self.location.href).pathname;
importScripts(BASE + 'static/private-data-registry.js');
importScripts(BASE + 'static/background-issue-sync.js');
const CACHE = 'stackchain-dashboard-shell-v116';
const CACHE = 'stackchain-dashboard-shell-v117';
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;