1 parent
718f0d1209
commit
5ec4dd7f6e
19 files changed
+224
-63
No files matched your search
+38
-32
@@ -3,6 +3,7 @@
|
||||
await workspaceLifecycle.optionalReady;
|
||||
const progressiveCaptureHandoff = window.stackchainProgressiveCapture?.handoff?.();
|
||||
const progressiveWorkHandoff = window.stackchainProgressiveMyWork?.handoff?.();
|
||||
const progressiveHumanGatesHandoff = window.stackchainProgressiveHumanGates?.handoff?.();
|
||||
window.stackchainProgressiveMyWork?.stop();
|
||||
const qs = (s, el=document) => el.querySelector(s);
|
||||
const announceWork = message => qs('#my-work-action-status').textContent = message;
|
||||
@@ -636,53 +637,58 @@
|
||||
return payload;
|
||||
}
|
||||
|
||||
const humanGates = createHumanGates({
|
||||
const humanGatesOnChange = (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();
|
||||
};
|
||||
const humanGates = progressiveHumanGatesHandoff?.controller || createHumanGates({
|
||||
storage:localStorage,
|
||||
getLogin:()=>planningOwnerLogin,
|
||||
getAccountKey:()=>planningOwnerAccountKey,
|
||||
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();
|
||||
},
|
||||
onChange:humanGatesOnChange,
|
||||
nodes:{
|
||||
count:qs('#human-gates-count'), list:qs('#human-gates-list'),
|
||||
status:qs('#human-gates-status'), panel:qs('#human-gates'),
|
||||
detail:qs('#human-gate-detail'),
|
||||
},
|
||||
});
|
||||
progressiveHumanGatesHandoff?.adoptIdentity(planningOwnerLogin, planningOwnerAccountKey);
|
||||
humanGates.setOnChange?.(humanGatesOnChange);
|
||||
const openHumanGates = () => humanGates.open().catch(error => {
|
||||
qs('#human-gates-status').textContent = error.message || 'Human Gates are unavailable.';
|
||||
});
|
||||
qs('#open-human-gates').addEventListener('click', openHumanGates);
|
||||
qs('#close-human-gates').addEventListener('click', () => {
|
||||
qs('#human-gates').hidden = true;
|
||||
if (window.location.hash === '#/my-work/human-gates') window.history.replaceState({}, '', '#/my-work');
|
||||
});
|
||||
qs('#human-gates-list').addEventListener('click', event => {
|
||||
const card = event.target.closest('[data-human-gate-id]');
|
||||
if (!card) return;
|
||||
humanGates.select(card.dataset.humanGateId);
|
||||
});
|
||||
qs('#human-gate-detail').addEventListener('click', event => {
|
||||
const decision = event.target.closest('[data-gate-decision]')?.dataset.gateDecision;
|
||||
if (!decision) return;
|
||||
const detail = qs('#human-gate-detail');
|
||||
const checklist = Object.fromEntries(Array.from(detail.querySelectorAll('[data-gate-checklist]')).map(input => [input.dataset.gateChecklist, input.checked]));
|
||||
humanGates.decideAndNext(decision, {
|
||||
checklist,
|
||||
reason:detail.querySelector('[data-gate-reason]')?.value || '',
|
||||
override_reason:detail.querySelector('[data-gate-override]')?.value || '',
|
||||
}).catch(error => { qs('#human-gates-status').textContent = error.message; });
|
||||
});
|
||||
humanGates.load().catch(() => {});
|
||||
if (window.location.hash === '#/my-work/human-gates') openHumanGates();
|
||||
if (!progressiveHumanGatesHandoff) {
|
||||
qs('#open-human-gates').addEventListener('click', openHumanGates);
|
||||
qs('#close-human-gates').addEventListener('click', () => {
|
||||
qs('#human-gates').hidden = true;
|
||||
if (window.location.hash === '#/my-work/human-gates') window.history.replaceState({}, '', '#/my-work');
|
||||
});
|
||||
qs('#human-gates-list').addEventListener('click', event => {
|
||||
const card = event.target.closest('[data-human-gate-id]');
|
||||
if (!card) return;
|
||||
humanGates.select(card.dataset.humanGateId);
|
||||
});
|
||||
qs('#human-gate-detail').addEventListener('click', event => {
|
||||
const decision = event.target.closest('[data-gate-decision]')?.dataset.gateDecision;
|
||||
if (!decision) return;
|
||||
const detail = qs('#human-gate-detail');
|
||||
const checklist = Object.fromEntries(Array.from(detail.querySelectorAll('[data-gate-checklist]')).map(input => [input.dataset.gateChecklist, input.checked]));
|
||||
humanGates.decideAndNext(decision, {
|
||||
checklist,
|
||||
reason:detail.querySelector('[data-gate-reason]')?.value || '',
|
||||
override_reason:detail.querySelector('[data-gate-override]')?.value || '',
|
||||
}).catch(error => { qs('#human-gates-status').textContent = error.message; });
|
||||
});
|
||||
}
|
||||
if (!progressiveHumanGatesHandoff?.started) humanGates.load().catch(() => {});
|
||||
if (window.location.hash === '#/my-work/human-gates' && !progressiveHumanGatesHandoff?.started) openHumanGates();
|
||||
|
||||
function syncCompletedFiledReviews() {
|
||||
if (!planningOwnerLogin) return Promise.resolve(false);
|
||||
|
||||
@@ -13,6 +13,7 @@ function createHumanGates(options = {}) {
|
||||
let loadEpoch = 0;
|
||||
const decisionKeys = new Map();
|
||||
let decisionFlight = null;
|
||||
let onChange = options.onChange;
|
||||
|
||||
const escape = value => String(value ?? '').replace(/[&<>"']/g, character => ({
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
||||
@@ -20,7 +21,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);
|
||||
const publish = state => onChange?.(JSON.parse(JSON.stringify(queue)), state);
|
||||
|
||||
function validSnapshot(value) {
|
||||
return value && Number.isInteger(value.pending_count) && Array.isArray(value.items) ? value : null;
|
||||
@@ -207,6 +208,7 @@ function createHumanGates(options = {}) {
|
||||
|
||||
return {
|
||||
load, open, reviewNext, select, decideAndNext, current,
|
||||
setOnChange(callback) { onChange = callback; },
|
||||
restoreCached: restore,
|
||||
snapshot: () => JSON.parse(JSON.stringify(queue)),
|
||||
route: () => location.hash,
|
||||
|
||||
@@ -2412,6 +2412,7 @@
|
||||
<script src="static/mobile-find-work-nav.js"></script>
|
||||
<script src="static/workspace-bootstrap.js"></script>
|
||||
<script src="static/human-gates.js"></script>
|
||||
<script src="static/progressive-human-gates.js"></script>
|
||||
<script src="static/dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,100 @@
|
||||
function createProgressiveHumanGates(options = {}) {
|
||||
const document = options.document || globalThis.document;
|
||||
const location = options.location || globalThis.location;
|
||||
const history = options.history || globalThis.history;
|
||||
const storage = options.storage || globalThis.localStorage;
|
||||
const isOnline = options.isOnline || (() => globalThis.navigator?.onLine !== false);
|
||||
const fetchJson = options.fetchJson || (async (path, requestOptions = {}) => {
|
||||
const response = await fetch(path, requestOptions);
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(payload.error || payload.detail?.message || payload.detail || 'Review request failed.');
|
||||
return payload;
|
||||
});
|
||||
const getIdentity = options.getIdentity || (async () => {
|
||||
const response = await fetchJson('api/v1/live', {headers:{Accept:'application/json'}});
|
||||
const user = response?.context?.user || {};
|
||||
const login = String(user.login || '').trim();
|
||||
return {login, accountKey:login && user.id ? String(user.id) + ':' + login : login};
|
||||
});
|
||||
const query = selector => document.querySelector(selector);
|
||||
const nodes = {
|
||||
count:query('#human-gates-count'), list:query('#human-gates-list'),
|
||||
status:query('#human-gates-status'), panel:query('#human-gates'),
|
||||
detail:query('#human-gate-detail'),
|
||||
};
|
||||
let login = '';
|
||||
let accountKey = '';
|
||||
let started = false;
|
||||
let startFlight = null;
|
||||
|
||||
const controller = createHumanGates({
|
||||
storage, isOnline, location, fetchJson,
|
||||
getLogin:() => login, getAccountKey:() => accountKey, nodes,
|
||||
});
|
||||
|
||||
const showError = error => {
|
||||
if (nodes.status) nodes.status.textContent = error?.message || 'Human Gates are unavailable.';
|
||||
};
|
||||
const open = () => start(true).catch(showError);
|
||||
query('#open-human-gates')?.addEventListener?.('click', open);
|
||||
query('#close-human-gates')?.addEventListener?.('click', () => {
|
||||
if (nodes.panel) nodes.panel.hidden = true;
|
||||
if (location.hash === '#/my-work/human-gates') history.replaceState({}, '', '#/my-work');
|
||||
});
|
||||
nodes.list?.addEventListener?.('click', event => {
|
||||
const card = event.target?.closest?.('[data-human-gate-id]');
|
||||
if (card) controller.select(card.dataset.humanGateId);
|
||||
});
|
||||
nodes.detail?.addEventListener?.('click', event => {
|
||||
const decision = event.target?.closest?.('[data-gate-decision]')?.dataset.gateDecision;
|
||||
if (!decision) return;
|
||||
const checklist = Object.fromEntries(Array.from(nodes.detail.querySelectorAll('[data-gate-checklist]')).map(input => [input.dataset.gateChecklist, input.checked]));
|
||||
controller.decideAndNext(decision, {
|
||||
checklist,
|
||||
reason:nodes.detail.querySelector?.('[data-gate-reason]')?.value || '',
|
||||
override_reason:nodes.detail.querySelector?.('[data-gate-override]')?.value || '',
|
||||
}).catch(showError);
|
||||
});
|
||||
|
||||
async function start(force = false) {
|
||||
if (!force && location.hash !== '#/my-work/human-gates') return false;
|
||||
if (started) {
|
||||
if (nodes.panel) nodes.panel.hidden = false;
|
||||
return true;
|
||||
}
|
||||
if (startFlight) return startFlight;
|
||||
startFlight = (async () => {
|
||||
const identity = await getIdentity();
|
||||
login = String(identity?.login || '').trim();
|
||||
accountKey = String(identity?.accountKey || login).trim();
|
||||
if (!login) throw new Error('Authenticated account identity is required.');
|
||||
await controller.open();
|
||||
started = true;
|
||||
return true;
|
||||
})();
|
||||
try { return await startFlight; }
|
||||
finally { startFlight = null; }
|
||||
}
|
||||
|
||||
return {
|
||||
start,
|
||||
handoff() {
|
||||
return {
|
||||
controller, started,
|
||||
adoptIdentity(nextLogin, nextAccountKey) {
|
||||
login = String(nextLogin || '').trim();
|
||||
accountKey = String(nextAccountKey || login).trim();
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||
window.stackchainProgressiveHumanGates = createProgressiveHumanGates({document});
|
||||
void window.stackchainProgressiveHumanGates.start().catch(() => {});
|
||||
}
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
globalThis.createHumanGates = globalThis.createHumanGates || require('./human-gates.js');
|
||||
module.exports = createProgressiveHumanGates;
|
||||
}
|
||||
@@ -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-v140';
|
||||
const CACHE = 'stackchain-dashboard-shell-v141';
|
||||
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;
|
||||
@@ -152,6 +152,7 @@ const SHELL = [
|
||||
BASE + 'static/dashboard.css',
|
||||
BASE + 'static/dashboard.js',
|
||||
BASE + 'static/human-gates.js',
|
||||
BASE + 'static/progressive-human-gates.js',
|
||||
BASE + 'static/icons/stackchain-192.png',
|
||||
BASE + 'static/icons/stackchain-512.png',
|
||||
BASE + 'static/session.js',
|
||||
|
||||
Reference in new issue
Block a user