stackchain-dashboard/frontend/progressive-human-gates.js
timmy 5ec4dd7f6e
All checks were successful
CI / lint (pull_request) Successful in 4m13s
CI / build-release (pull_request) Successful in 8s
CI / browser-journey (pull_request) Successful in 7m51s
CI / release-candidate (pull_request) Has been skipped
feat: open Human Gates before optional hydration
Closes #1419
2026-08-26 04:52:14 +00:00

101 lines
4.1 KiB
JavaScript

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;
}