feat: surface Human Gates in mobile preparation (Closes #1417)
This commit is contained in:
1 parent
e9d257e203
commit
03b92f21c3
21 files changed
+202
-43
No files matched your search
@@ -163,6 +163,7 @@
|
||||
}
|
||||
const mobileQueueLauncher = createMobileQueueLauncher({
|
||||
openDelivery: () => mobileDeliveryRecovery.open(),
|
||||
openHumanGates: () => openHumanGates(),
|
||||
openToday: () => mobileWorkEntry.open(),
|
||||
openAgenda: openAgendaSession,
|
||||
openUpdates: openUpdateTriage,
|
||||
@@ -642,6 +643,14 @@
|
||||
isOnline:()=>navigator.onLine,
|
||||
location:window.location,
|
||||
fetchJson:fetchReviewJson,
|
||||
onChange:(snapshot, state)=>{
|
||||
queueCounts.gate = snapshot.pending_count;
|
||||
queueCounts.gateUnavailable = state.available === false;
|
||||
preparationItems.gate = snapshot.items;
|
||||
mobileTaskDock.updateQueues(queueCounts);
|
||||
if (state.authoritative) mobileStartDay.reconcile({authoritative:true, authoritativePhases:['gate']});
|
||||
mobileStartDay.render();
|
||||
},
|
||||
nodes:{
|
||||
count:qs('#human-gates-count'), list:qs('#human-gates-list'),
|
||||
status:qs('#human-gates-status'), panel:qs('#human-gates'),
|
||||
@@ -3490,8 +3499,11 @@
|
||||
counts.later = laterMyWork.length;
|
||||
counts.draft = lastDrafts.length;
|
||||
counts.delivery = draftInbox.partition(lastDrafts).actionable;
|
||||
counts.gate = queueCounts.gate;
|
||||
counts.gateUnavailable = queueCounts.gateUnavailable;
|
||||
preparationItems = {
|
||||
delivery:draftInbox.partition(lastDrafts).deliveries,
|
||||
gate:preparationItems.gate || [],
|
||||
agenda:agendaMyWork(activeMyWork),
|
||||
attention:activeMyWork.filter(item => item.needs_attention),
|
||||
update:activeMyWork.filter(item => item.has_update),
|
||||
|
||||
+12
-2
@@ -20,6 +20,7 @@ function createHumanGates(options = {}) {
|
||||
const cacheKey = () => 'stackchain.human-gates.v1:' + String(getAccountKey() || '').trim().toLowerCase();
|
||||
const setText = (node, value) => { if (node) node.textContent = value; };
|
||||
const setHtml = (node, value) => { if (node) node.innerHTML = value; };
|
||||
const publish = state => options.onChange?.(JSON.parse(JSON.stringify(queue)), state);
|
||||
|
||||
function validSnapshot(value) {
|
||||
return value && Number.isInteger(value.pending_count) && Array.isArray(value.items) ? value : null;
|
||||
@@ -91,7 +92,11 @@ function createHumanGates(options = {}) {
|
||||
render();
|
||||
}
|
||||
const cached = restore();
|
||||
if (cached) { queue = cached; render(); }
|
||||
if (cached) {
|
||||
queue = cached;
|
||||
render();
|
||||
publish({available:true, authoritative:false, cached:true});
|
||||
}
|
||||
try {
|
||||
const live = validSnapshot(await fetchJson('api/v1/human-gates'));
|
||||
if (epoch !== loadEpoch || accountKey !== loadedAccountKey) return queue;
|
||||
@@ -99,10 +104,14 @@ function createHumanGates(options = {}) {
|
||||
queue = { pending_count: live.pending_count, items: live.items.slice() };
|
||||
save(queue);
|
||||
render();
|
||||
publish({available:true, authoritative:true});
|
||||
return queue;
|
||||
} catch (error) {
|
||||
if (epoch !== loadEpoch || accountKey !== loadedAccountKey) return queue;
|
||||
if (!cached) throw error;
|
||||
if (!cached) {
|
||||
publish({available:false, authoritative:false});
|
||||
throw error;
|
||||
}
|
||||
setText(nodes.status, 'Offline cached gate list · reconnect before deciding.');
|
||||
return queue;
|
||||
}
|
||||
@@ -175,6 +184,7 @@ function createHumanGates(options = {}) {
|
||||
queue.items = queue.items.filter(candidate => candidate.id !== item.id);
|
||||
queue.pending_count = Math.max(0, queue.pending_count - 1);
|
||||
save(queue); render();
|
||||
publish({available:true, authoritative:true, decision:true});
|
||||
reviewIndex += 1;
|
||||
const next = current();
|
||||
if (next) reviewNext(); else renderDetail(null);
|
||||
|
||||
@@ -2180,6 +2180,7 @@
|
||||
<button data-mobile-queue="week" type="button"><span><strong>Week Ahead</strong><small id="mobile-week-summary" aria-live="polite">Nothing planned</small></span></button>
|
||||
<button data-mobile-queue="agenda" type="button"><span><strong>Agenda</strong><small>Upcoming deadlines</small></span><span data-mobile-queue-count="agenda">0</span></button>
|
||||
<button data-mobile-queue="delivery" type="button"><span><strong>Delivery</strong><small>Needs recovery</small></span><span data-mobile-queue-count="delivery">0</span></button>
|
||||
<button data-mobile-queue="gate" type="button"><span><strong>Human Gates</strong><small>Release decisions</small></span><span data-mobile-queue-count="gate">0</span></button>
|
||||
<button data-mobile-queue="attention" type="button"><span><strong>Attention</strong><small>Needs a response</small></span><span data-mobile-queue-count="attention">0</span></button>
|
||||
<button data-mobile-queue="update" type="button" aria-label="Updates, 0 unread conversations"><span><strong>Updates</strong><small>Unread conversations</small></span><span data-mobile-queue-count="update">0</span></button>
|
||||
<button data-mobile-queue="following" type="button" aria-label="Following, 0 unseen changes"><span><strong>Following</strong><small>Issues and pull requests you watch</small></span><span data-mobile-queue-count="following">0</span></button>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
};
|
||||
const continuation = [
|
||||
['delivery', 'Recover Delivery'],
|
||||
['gate', 'Review Human Gates'],
|
||||
['attention', 'Start Attention'],
|
||||
['today', 'Continue Today'],
|
||||
['update', 'Resume Updates'],
|
||||
@@ -30,6 +31,7 @@
|
||||
|
||||
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();
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
const storage = options.storage || (typeof localStorage !== 'undefined' ? localStorage : null);
|
||||
const reviewOrder = [
|
||||
['delivery', 'Delivery recovery'],
|
||||
['gate', 'Human Gates'],
|
||||
['agenda', 'Agenda'],
|
||||
['attention', 'Attention'],
|
||||
['update', 'Updates'],
|
||||
@@ -104,6 +105,12 @@
|
||||
function briefing() {
|
||||
const counts = options.getCounts ? options.getCounts() : {};
|
||||
let phases = reviewPhases(counts);
|
||||
const gateUnavailable = counts.gateUnavailable === true;
|
||||
if (gateUnavailable) {
|
||||
phases = phases.filter(phase => phase.name !== 'gate');
|
||||
const afterDelivery = phases[0]?.name === 'delivery' ? 1 : 0;
|
||||
phases.splice(afterDelivery, 0, {name:'gate', label:'Human Gates unavailable · retry', count:count(counts.gate)});
|
||||
}
|
||||
const followingUnavailable = counts.followingUnavailable === true;
|
||||
if (followingUnavailable) {
|
||||
phases = phases.filter(phase => phase.name !== 'following');
|
||||
@@ -115,14 +122,17 @@
|
||||
const other = total - delivery;
|
||||
const next = phases.length ? phases[0].name : (today ? 'today' : 'find');
|
||||
const nextLabel = next === 'delivery' ? 'Review Delivery' :
|
||||
(next === 'gate' && gateUnavailable ? 'Retry Human Gates' :
|
||||
(next === 'following' && followingUnavailable ? 'Retry Following' :
|
||||
(phases.length ? 'Review ' + phases[0].label : (today ? 'Continue Today' : 'Find Work')));
|
||||
(phases.length ? 'Review ' + phases[0].label : (today ? 'Continue Today' : 'Find Work'))));
|
||||
const gateRetry = next === 'gate' && gateUnavailable;
|
||||
const followingRetry = next === 'following' && followingUnavailable;
|
||||
return {
|
||||
total,
|
||||
next,
|
||||
label: nextLabel,
|
||||
summary: followingRetry ? 'Following needs retry before Today · ' + today + ' planned' :
|
||||
summary: gateRetry ? 'Human Gates need retry before Today · ' + today + ' planned' :
|
||||
followingRetry ? 'Following needs retry before Today · ' + today + ' planned' :
|
||||
delivery ? delivery + (delivery === 1 ? ' delivery needs' : ' deliveries need') +
|
||||
' action before Today' + (other ? ' · ' + other + ' other ' + (other === 1 ? 'item' : 'items') : '') +
|
||||
' · ' + today + ' planned' :
|
||||
|
||||
@@ -97,9 +97,9 @@
|
||||
}
|
||||
|
||||
function updateQueues(counts) {
|
||||
const names = 'today agenda delivery attention update filed later draft'.split(' ');
|
||||
const names = 'today agenda delivery gate attention update filed later draft'.split(' ');
|
||||
const normalized = Object.fromEntries(names.map(name => [name, Math.max(0, Number(counts?.[name]) || 0)]));
|
||||
const actionableNames = ['today', 'delivery', 'attention', 'update', 'filed', 'later', 'draft'];
|
||||
const actionableNames = ['today', 'delivery', 'gate', 'attention', 'update', 'filed', 'later', 'draft'];
|
||||
const active = actionableNames.reduce((total, name) => total + (normalized[name] > 0 ? 1 : 0), 0);
|
||||
Object.entries(options.queueCounts || {}).forEach(([name, element]) => {
|
||||
element.textContent = String(normalized[name] || 0);
|
||||
@@ -124,6 +124,7 @@
|
||||
: 'Queues: Today ' + normalized.today
|
||||
+ ', Agenda ' + normalized.agenda + ' due'
|
||||
+ ', Delivery ' + normalized.delivery
|
||||
+ ', Human Gates ' + normalized.gate
|
||||
+ ', Attention ' + normalized.attention
|
||||
+ ', Updates ' + normalized.update
|
||||
+ ', Filed ' + normalized.filed
|
||||
|
||||
@@ -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-v139';
|
||||
const CACHE = 'stackchain-dashboard-shell-v140';
|
||||
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;
|
||||
|
||||
Reference in new issue
Block a user