feat: resume Human Gate evidence reviews (Closes #1435)
This commit is contained in:
1 parent
1e9c3444fa
commit
aba217f89b
16 files changed
+214
-40
No files matched your search
+47
-8
@@ -20,6 +20,8 @@ function createHumanGates(options = {}) {
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
||||
})[character]);
|
||||
const cacheKey = () => 'stackchain.human-gates.v1:' + String(getAccountKey() || '').trim().toLowerCase();
|
||||
const progressKey = item => 'stackchain.human-gate-review.v1:' +
|
||||
String(getAccountKey() || '').trim().toLowerCase() + ':' + item.id + ':' + item.revision;
|
||||
const setText = (node, value) => { if (node) node.textContent = value; };
|
||||
const setHtml = (node, value) => { if (node) node.innerHTML = value; };
|
||||
const publish = state => onChange?.(JSON.parse(JSON.stringify(queue)), state);
|
||||
@@ -38,6 +40,40 @@ function createHumanGates(options = {}) {
|
||||
try { storage.setItem(cacheKey(), JSON.stringify(value)); } catch (_) {}
|
||||
}
|
||||
|
||||
function restoreProgress(item) {
|
||||
if (!getLogin() || !item) return null;
|
||||
try {
|
||||
const value = JSON.parse(storage.getItem(progressKey(item)) || 'null');
|
||||
if (!value || value.gate_id !== item.id || value.revision !== item.revision) return null;
|
||||
return value;
|
||||
} catch (_) { return null; }
|
||||
}
|
||||
|
||||
function saveProgress(values = {}) {
|
||||
const item = current();
|
||||
if (!getLogin() || !item) return false;
|
||||
const checklist = values.checklist || {};
|
||||
const progress = {
|
||||
gate_id:item.id, revision:item.revision,
|
||||
checklist:Object.fromEntries(['exact_hash', 'artifacts_reviewed', 'provenance_reviewed'].map(key => [key, checklist[key] === true])),
|
||||
reason:String(values.reason || ''), override_reason:String(values.override_reason || ''),
|
||||
};
|
||||
try { storage.setItem(progressKey(item), JSON.stringify(progress)); return true; } catch (_) { return false; }
|
||||
}
|
||||
|
||||
function captureProgress() {
|
||||
if (!nodes.detail?.querySelectorAll) return false;
|
||||
const checklist = Object.fromEntries(Array.from(nodes.detail.querySelectorAll('[data-gate-checklist]')).map(input => [input.dataset.gateChecklist, input.checked]));
|
||||
return saveProgress({
|
||||
checklist,
|
||||
reason:nodes.detail.querySelector?.('[data-gate-reason]')?.value || '',
|
||||
override_reason:nodes.detail.querySelector?.('[data-gate-override]')?.value || '',
|
||||
});
|
||||
}
|
||||
|
||||
nodes.detail?.addEventListener?.('input', captureProgress);
|
||||
nodes.detail?.addEventListener?.('change', captureProgress);
|
||||
|
||||
function render() {
|
||||
setText(nodes.count, String(queue.pending_count));
|
||||
if (!queue.pending_count) {
|
||||
@@ -58,11 +94,13 @@ function createHumanGates(options = {}) {
|
||||
setHtml(nodes.detail, '<div class="human-gates-zero"><strong>Inbox zero</strong><span>Fixed review snapshot complete.</span></div>');
|
||||
return;
|
||||
}
|
||||
const progress = restoreProgress(item) || {checklist:{}, reason:'', override_reason:''};
|
||||
const checked = key => progress.checklist?.[key] === true ? ' checked' : '';
|
||||
const checks = (item.checks || []).map(check =>
|
||||
'<li class="gate-check gate-check-' + escape(check.state) + '"><strong>' + escape(check.name) + '</strong> · ' + escape(check.state) + (check.required ? ' · required' : '') + '</li>'
|
||||
).join('');
|
||||
const artifacts = (item.artifacts || []).map(artifact => '<li><a href="' + escape(artifact.url) + '" rel="noreferrer">' + escape(artifact.name) + '</a></li>').join('');
|
||||
const links = (item.links || []).map(link => '<li><a href="' + escape(link.url) + '" rel="noreferrer">' + escape(link.label) + '</a></li>').join('');
|
||||
const artifacts = (item.artifacts || []).map(artifact => '<li><a href="' + escape(artifact.url) + '" target="_blank" rel="noreferrer noopener">' + escape(artifact.name) + '</a></li>').join('');
|
||||
const links = (item.links || []).map(link => '<li><a href="' + escape(link.url) + '" target="_blank" rel="noreferrer noopener">' + escape(link.label) + '</a></li>').join('');
|
||||
const provenance = Object.entries(item.provenance || {}).map(([key, value]) => '<li><strong>' + escape(key) + '</strong> · ' + escape(value) + '</li>').join('');
|
||||
const history = (item.history || []).map(event => '<li><strong>' + escape(event.action) + '</strong> · ' + escape(event.at) + '</li>').join('');
|
||||
setHtml(nodes.detail,
|
||||
@@ -73,11 +111,11 @@ function createHumanGates(options = {}) {
|
||||
'<h4>Artifacts</h4><ul>' + artifacts + '</ul><h4>Links</h4><ul>' + links + '</ul>' +
|
||||
'<h4>Checks</h4><ul>' + checks + '</ul><h4>Provenance</h4><ul>' + provenance + '</ul>' +
|
||||
'<h4>History</h4><ul>' + history + '</ul>' +
|
||||
'<label><input type="checkbox" data-gate-checklist="exact_hash"> Exact hash reviewed</label>' +
|
||||
'<label><input type="checkbox" data-gate-checklist="artifacts_reviewed"> Artifacts reviewed</label>' +
|
||||
'<label><input type="checkbox" data-gate-checklist="provenance_reviewed"> Provenance reviewed</label>' +
|
||||
'<label>Hold reason<textarea data-gate-reason></textarea></label>' +
|
||||
'<label>Override reason<textarea data-gate-override></textarea></label>' +
|
||||
'<label><input type="checkbox" data-gate-checklist="exact_hash"' + checked('exact_hash') + '> Exact hash reviewed</label>' +
|
||||
'<label><input type="checkbox" data-gate-checklist="artifacts_reviewed"' + checked('artifacts_reviewed') + '> Artifacts reviewed</label>' +
|
||||
'<label><input type="checkbox" data-gate-checklist="provenance_reviewed"' + checked('provenance_reviewed') + '> Provenance reviewed</label>' +
|
||||
'<label>Hold reason<textarea data-gate-reason>' + escape(progress.reason) + '</textarea></label>' +
|
||||
'<label>Override reason<textarea data-gate-override>' + escape(progress.override_reason) + '</textarea></label>' +
|
||||
'<div><button type="button" data-gate-decision="release">Release & next</button>' +
|
||||
'<button type="button" data-gate-decision="hold">Hold & next</button></div></article>'
|
||||
);
|
||||
@@ -183,6 +221,7 @@ function createHumanGates(options = {}) {
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
decisionKeys.delete(decisionKey.operation);
|
||||
try { storage.removeItem?.(progressKey(item)); } catch (_) {}
|
||||
queue.items = queue.items.filter(candidate => candidate.id !== item.id);
|
||||
queue.pending_count = Math.max(0, queue.pending_count - 1);
|
||||
save(queue); render();
|
||||
@@ -220,7 +259,7 @@ function createHumanGates(options = {}) {
|
||||
}
|
||||
|
||||
return {
|
||||
load, open, reviewNext, select, decideAndNext, current,
|
||||
load, open, reviewNext, select, decideAndNext, current, saveProgress,
|
||||
setOnChange(callback) { onChange = callback; },
|
||||
restoreCached: restore,
|
||||
snapshot: () => JSON.parse(JSON.stringify(queue)),
|
||||
|
||||
@@ -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-v145';
|
||||
const CACHE = 'stackchain-dashboard-shell-v146';
|
||||
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