fix: harden Human Gates review flow
This commit is contained in:
1 parent
364fc60116
commit
dbb8ff4d4e
8 files changed
+130
-16
No files matched your search
@@ -392,6 +392,7 @@
|
||||
let editingOutboxId = null;
|
||||
let confirmedOwnerLogin = '';
|
||||
let planningOwnerLogin = '';
|
||||
let planningOwnerAccountKey = '';
|
||||
let activeFlushLogin = '';
|
||||
let rR = null;
|
||||
function rRC() {
|
||||
@@ -637,6 +638,7 @@
|
||||
const humanGates = createHumanGates({
|
||||
storage:localStorage,
|
||||
getLogin:()=>planningOwnerLogin,
|
||||
getAccountKey:()=>planningOwnerAccountKey,
|
||||
isOnline:()=>navigator.onLine,
|
||||
location:window.location,
|
||||
fetchJson:fetchReviewJson,
|
||||
@@ -655,8 +657,9 @@
|
||||
if (window.location.hash === '#/my-work/human-gates') window.history.replaceState({}, '', '#/my-work');
|
||||
});
|
||||
qs('#human-gates-list').addEventListener('click', event => {
|
||||
if (!event.target.closest('[data-human-gate-id]')) return;
|
||||
humanGates.reviewNext();
|
||||
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;
|
||||
@@ -5564,6 +5567,8 @@
|
||||
const retainedPlanningLogin = !snapshot.context.error ?
|
||||
String(snapshot.context.user?.login || '').trim() : '';
|
||||
planningOwnerLogin = retainedPlanningLogin;
|
||||
planningOwnerAccountKey = retainedPlanningLogin && snapshot.context.user?.id ?
|
||||
String(snapshot.context.user.id) + ':' + retainedPlanningLogin : '';
|
||||
updatePlanningAvailability();
|
||||
if (planningOwnerLogin) {
|
||||
syncPendingTomorrow();
|
||||
|
||||
+22
-5
@@ -1,6 +1,7 @@
|
||||
function createHumanGates(options = {}) {
|
||||
const storage = options.storage || window.localStorage;
|
||||
const getLogin = options.getLogin || (() => '');
|
||||
const getAccountKey = options.getAccountKey || getLogin;
|
||||
const isOnline = options.isOnline || (() => navigator.onLine);
|
||||
const location = options.location || window.location;
|
||||
const fetchJson = options.fetchJson;
|
||||
@@ -8,11 +9,12 @@ function createHumanGates(options = {}) {
|
||||
let queue = { pending_count: 0, items: [] };
|
||||
let reviewSnapshot = [];
|
||||
let reviewIndex = -1;
|
||||
const decisionKeys = new Map();
|
||||
|
||||
const escape = value => String(value ?? '').replace(/[&<>"']/g, character => ({
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
||||
})[character]);
|
||||
const cacheKey = () => 'stackchain.human-gates.v1:' + String(getLogin() || '').trim().toLowerCase();
|
||||
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; };
|
||||
|
||||
@@ -111,10 +113,22 @@ function createHumanGates(options = {}) {
|
||||
return item;
|
||||
}
|
||||
|
||||
function select(gateId) {
|
||||
if (reviewIndex < 0) reviewSnapshot = queue.items.slice();
|
||||
const index = reviewSnapshot.findIndex(item => item.id === gateId);
|
||||
if (index < 0) throw new Error('Gate is not in the current review snapshot.');
|
||||
reviewIndex = index;
|
||||
return reviewNext();
|
||||
}
|
||||
|
||||
function current() { return reviewIndex < 0 ? null : (reviewSnapshot[reviewIndex] || null); }
|
||||
function idempotencyKey(item, decision) {
|
||||
function idempotencyKey(item, decision, payload) {
|
||||
const operation = item.id + ':' + item.revision + ':' + decision + ':' + JSON.stringify(payload);
|
||||
if (decisionKeys.has(operation)) return { operation, key: decisionKeys.get(operation) };
|
||||
const nonce = globalThis.crypto?.randomUUID?.() || (Date.now().toString(36) + '-' + Math.random().toString(36).slice(2));
|
||||
return 'human-gate:' + item.id + ':' + item.revision + ':' + decision + ':' + nonce;
|
||||
const key = 'human-gate:' + item.id + ':' + item.revision + ':' + decision + ':' + nonce;
|
||||
decisionKeys.set(operation, key);
|
||||
return { operation, key };
|
||||
}
|
||||
|
||||
async function decideAndNext(decision, values = {}) {
|
||||
@@ -135,11 +149,13 @@ function createHumanGates(options = {}) {
|
||||
reason: String(values.reason || '').trim(),
|
||||
override_reason: String(values.override_reason || '').trim(), checklist,
|
||||
};
|
||||
const decisionKey = idempotencyKey(item, decision, payload);
|
||||
const receipt = await fetchJson('api/v1/human-gates/' + encodeURIComponent(item.id) + '/decision', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey(item, decision) },
|
||||
headers: { 'Content-Type': 'application/json', 'Idempotency-Key': decisionKey.key },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
decisionKeys.delete(decisionKey.operation);
|
||||
queue.items = queue.items.filter(candidate => candidate.id !== item.id);
|
||||
queue.pending_count = Math.max(0, queue.pending_count - 1);
|
||||
save(queue); render();
|
||||
@@ -157,7 +173,8 @@ function createHumanGates(options = {}) {
|
||||
}
|
||||
|
||||
return {
|
||||
load, open, reviewNext, decideAndNext, current,
|
||||
load, open, reviewNext, select, decideAndNext, current,
|
||||
restoreCached: restore,
|
||||
snapshot: () => JSON.parse(JSON.stringify(queue)),
|
||||
route: () => location.hash,
|
||||
};
|
||||
|
||||
+1
-1
@@ -187,7 +187,7 @@
|
||||
<button class="end-today-session" id="end-today-session" type="button" hidden>End session</button>
|
||||
<button class="find-work-action" id="find-work" type="button">Find work</button>
|
||||
<button class="new-issue" id="new-issue" type="button">New issue</button>
|
||||
<button class="human-gates-launcher" id="open-human-gates" type="button">Human Gates <span id="human-gates-count">0</span></button>
|
||||
<button class="human-gates-launcher" id="open-human-gates" type="button">Review next <span id="human-gates-count">0</span></button>
|
||||
</div>
|
||||
<section id="human-gates" class="human-gates" aria-labelledby="human-gates-heading" hidden>
|
||||
<div class="human-gates-header">
|
||||
|
||||
Reference in new issue
Block a user