feat: guide mobile delivery recovery (Closes #1014)
CI / lint (pull_request) Successful in 2m43s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 2m4s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-17 09:04:41 +00:00
1 parent c0a02a05d8
commit 1e4f101219
9 files changed
+353 -12

No files matched your search

+8
View File
@@ -1031,6 +1031,14 @@ textarea { resize: vertical; min-height: 120px; }
.mobile-queue-panel { padding:16px; padding-bottom:calc(16px + env(safe-area-inset-bottom)); }
.mobile-queue-panel header { display:flex; align-items:center; justify-content:space-between; gap:12px; }
.mobile-queue-panel h2 { margin:0; }
.mobile-delivery-recovery { box-sizing:border-box; width:100%; max-width:none; max-height:100dvh; margin:auto 0 0; padding:0; border:0; border-radius:18px 18px 0 0; color:var(--text); background:#102641; }
.mobile-delivery-recovery::backdrop { background:rgba(3,9,18,.78); }
.mobile-delivery-recovery-panel { box-sizing:border-box; display:grid; gap:12px; width:100%; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); overflow-wrap:anywhere; }
.mobile-delivery-recovery-panel header { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; }
.mobile-delivery-recovery-panel h2, .mobile-delivery-recovery-panel p { margin:0; }
.mobile-delivery-recovery-panel button { min-height:48px; }
.mobile-delivery-recovery-panel > #mobile-delivery-recovery-action { width:100%; font-weight:800; }
.delivery-recovery-reason { padding:12px; border:1px solid #31577f; border-radius:12px; background:#0b1c30; }
.mobile-start-day { display:grid; gap:12px; max-width:100%; overflow-wrap:anywhere; margin-top:12px; padding:14px; border:1px solid #31577f; border-radius:14px; background:linear-gradient(135deg,#173b64,#102641); }
.mobile-start-day h3, .mobile-start-day p { margin:0; }
.mobile-start-day p + p { margin-top:4px; }
+11 -9
View File
@@ -91,14 +91,15 @@
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';
}
const mobileDeliveryRecovery = createMobileDeliveryRecovery({
getItems: () => draftInbox.partition(lastDrafts).deliveries,
getIndex: item => lastDrafts.indexOf(item),
beforeOpen: () => selectMobileQueue('draft'),
onComplete: () => {
if (!mobileStartDay.completePhase('delivery')) showMobileQueueCompletion('Delivery', true);
},
});
mobileDeliveryRecovery.start();
function openFiledFollowUp() {
selectMobileQueue('filed');
const target = filedFollowUpTarget(completedFiledReview.visible(lastMyWork));
@@ -112,7 +113,7 @@
return target.kind === 'update' ? 'opened-update' : 'opened-issue';
}
const mobileQueueLauncher = createMobileQueueLauncher({
openDelivery: openDeliveryRecovery,
openDelivery: () => mobileDeliveryRecovery.open(),
openToday: () => mobileWorkEntry.open(),
openAgenda: openAgendaSession,
openUpdates: openUpdateTriage,
@@ -3338,6 +3339,7 @@
qs('#my-work-action-status').textContent = 'Draft discarded.';
});
});
mobileDeliveryRecovery.render();
}
function updateWorkPaginationControls() {
+15
View File
@@ -1543,6 +1543,20 @@
</section>
</dialog>
<dialog id="mobile-delivery-recovery" class="mobile-delivery-recovery" aria-labelledby="mobile-delivery-recovery-title">
<section class="mobile-delivery-recovery-panel">
<header>
<div><p class="small muted" id="mobile-delivery-recovery-progress"></p><h2 id="mobile-delivery-recovery-title">Recover delivery</h2></div>
<button id="close-mobile-delivery-recovery" type="button">Close</button>
</header>
<p id="mobile-delivery-recovery-destination" class="small"></p>
<p id="mobile-delivery-recovery-reason" class="delivery-recovery-reason"></p>
<p id="mobile-delivery-recovery-status" class="small" role="status" aria-live="polite"></p>
<button id="mobile-delivery-recovery-action" type="button">Recover delivery</button>
<details><summary>Other recovery options</summary><p class="small muted">Close this guide to edit, copy, or discard the saved delivery from Drafts.</p></details>
</section>
</dialog>
<nav class="mobile-task-dock" id="mobile-task-dock" aria-label="Primary tasks">
<button class="mobile-task-action" data-mobile-task="work" type="button" aria-current="page"><span id="mobile-work-label">Work</span></button>
<button class="mobile-task-action" data-mobile-task="find" type="button">Find</button>
@@ -1639,6 +1653,7 @@
<script src="static/mobile-pull-refresh.js"></script>
<script src="static/mobile-work-entry.js"></script>
<script src="static/mobile-queue-launcher.js"></script>
<script src="static/mobile-delivery-recovery.js"></script>
<script src="static/mobile-start-day.js"></script>
<script src="static/update-triage-session.js"></script>
<script src="static/update-review-handoff.js"></script>
+148
View File
@@ -0,0 +1,148 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) module.exports = factory;
else root.createMobileDeliveryRecovery = factory;
})(typeof self !== 'undefined' ? self : this, function createMobileDeliveryRecovery(options) {
const byId = id => typeof document === 'undefined' ? null : document.getElementById(id);
const elements = options.elements || (typeof document === 'undefined' ? null : {
dialog: byId('mobile-delivery-recovery'), close: byId('close-mobile-delivery-recovery'),
title: byId('mobile-delivery-recovery-title'), destination: byId('mobile-delivery-recovery-destination'),
reason: byId('mobile-delivery-recovery-reason'), progress: byId('mobile-delivery-recovery-progress'),
action: byId('mobile-delivery-recovery-action'), status: byId('mobile-delivery-recovery-status'),
});
const priority = {authorization: 0, attention: 1, uncertain: 2, waiting: 3};
function identity(item) {
return String(item?.outbox_id || item?.id || '');
}
function state(item) {
if (item?.status === 'authorization') return 'authorization';
if (item?.status === 'attention' || item?.checklist_conflict || item?.quarantined) return 'attention';
if (item?.delivery_state === 'uncertain') return 'uncertain';
return 'waiting';
}
function primary(item) {
const kind = state(item);
if (kind === 'authorization') {
return item?.outbox_kind === 'issue-close' ? 'Authorize & close' : 'Authorize & send';
}
if (item?.checklist_conflict) return 'Review changes';
if (kind === 'attention') return item?.quarantined ? 'Copy content' : 'Open current item';
if (kind === 'uncertain') return 'Verified not posted — retry';
return 'Retry now';
}
function ordered() {
return [...(options.getItems ? options.getItems() : [])].sort((left, right) => {
const byState = priority[state(left)] - priority[state(right)];
if (byState) return byState;
return identity(left).localeCompare(identity(right));
});
}
const stateLabels = {
authorization: 'Authorization required',
attention: 'Needs attention',
uncertain: 'Delivery uncertain',
waiting: 'Ready to retry',
};
let currentId = '';
let active = false;
function currentItem(items) {
return items.find(item => identity(item) === currentId) || items[0];
}
function snapshot() {
const items = ordered();
const item = currentItem(items);
if (item) currentId = identity(item);
else currentId = '';
const index = item ? items.indexOf(item) : -1;
return {
count: items.length,
current: item ? {
id: identity(item),
state: state(item),
primary: primary(item),
position: index + 1,
total: items.length,
} : null,
};
}
function render() {
const result = snapshot();
if (!elements) return result;
const items = ordered();
const item = currentItem(items);
if (!item) {
if (active) {
active = false;
if (elements.dialog?.open) elements.dialog.close();
if (options.onComplete) options.onComplete();
}
return result;
}
elements.title.textContent = item.title || 'Queued delivery';
elements.destination.textContent = [item.repository, item.details].filter(Boolean).join(' · ') || 'Saved delivery';
elements.reason.textContent = item.last_attempt_error || item.ownership || stateLabels[state(item)];
elements.progress.textContent = 'Delivery ' + result.current.position + ' of ' + result.current.total + ' · ' + stateLabels[state(item)];
elements.action.textContent = primary(item);
if (elements.status) elements.status.textContent = '';
return result;
}
function open() {
const queue = byId('mobile-queue-sheet');
if (queue?.open) queue.close();
if (options.beforeOpen) options.beforeOpen();
currentId = '';
const result = snapshot();
if (!result.current) return 'empty';
active = true;
render();
if (elements?.dialog && !elements.dialog.open) elements.dialog.showModal();
return 'opened';
}
async function defaultActivate(item) {
if (typeof document === 'undefined') return false;
const index = options.getIndex ? options.getIndex(item) : -1;
const selector = item.status === 'authorization' ? '.draft-authorize' :
item.checklist_conflict ? '.draft-review-checklist' : item.quarantined ? '.draft-copy' :
item.status === 'attention' ? '.draft-resume' : '.draft-send';
const action = document.querySelector('#my-work-list ' + selector + '[data-draft-index="' + index + '"]');
if (!action) {
if (elements.status) elements.status.textContent = 'This delivery changed. Reopen recovery to load its current action.';
return false;
}
action.click();
if (selector === '.draft-resume' || selector === '.draft-copy') elements.dialog.close();
await Promise.resolve();
return true;
}
async function activate() {
const items = ordered();
const item = currentItem(items);
const perform = options.activate || defaultActivate;
if (!item) return render();
if (elements?.action) elements.action.disabled = true;
if (elements?.status) elements.status.textContent = 'Working…';
try {
await perform(item, state(item));
} finally {
if (elements?.action) elements.action.disabled = false;
}
return render();
}
function start() {
if (elements?.action?.addEventListener) elements.action.addEventListener('click', activate);
if (elements?.close?.addEventListener) elements.close.addEventListener('click', () => elements.dialog.close());
}
return {activate, open, render, snapshot, start};
});
+1
View File
@@ -106,6 +106,7 @@ const SHELL = [
BASE + 'static/mobile-task-dock.js',
BASE + 'static/mobile-work-entry.js',
BASE + 'static/mobile-queue-launcher.js',
BASE + 'static/mobile-delivery-recovery.js',
BASE + 'static/mobile-start-day.js',
BASE + 'static/update-triage-session.js',
BASE + 'static/update-review-handoff.js',