fix: authorize queued issue closures explicitly (Closes #483)
CI / lint (pull_request) Successful in 1m1s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-10 12:29:02 +00:00
1 parent cc63fc6499
commit 62a0841eb5
16 files changed
+165 -67

No files matched your search

+1 -1
View File
@@ -241,7 +241,7 @@ function createAuthoredOutbox({ storage, fetchJson, coordinator, backgroundSync,
let blocked = 0;
currentLogin = String(currentLogin || '').trim();
for (const item of read()) {
if (item.status === 'attention') continue;
if (item.status === 'attention' || item.kind === 'issue-close') continue;
if (!currentLogin || item.ownerLogin !== currentLogin) { blocked += 1; continue; }
const outcome = await sendItem(item, currentLogin);
if (outcome.result) confirmed.push(outcome.result);
+2
View File
@@ -97,6 +97,7 @@ function createIssueSyncStore({
const timestamp = Number(now());
const items = await records.getAll();
const item = items.find(candidate => candidate.ownerLogin === ownerLogin &&
candidate.kind !== 'issue-close' &&
(candidate.status === 'queued' || candidate.status === 'sending') &&
(candidate.status !== 'sending' || Number(candidate.claimUntil) <= timestamp));
if (!item) return null;
@@ -114,6 +115,7 @@ function createIssueSyncStore({
const eligible = (await records.getAll()).filter(candidate =>
candidate.recordType !== 'receipt-preference' &&
candidate.ownerLogin === ownerLogin &&
candidate.kind !== 'issue-close' &&
(candidate.status === 'queued' || candidate.status === 'sending') &&
(candidate.status !== 'sending' || Number(candidate.claimUntil) <= timestamp));
const lanes = {
+21 -4
View File
@@ -1662,9 +1662,12 @@
'<button class="draft-resume" data-draft-index="' + index + '" type="button">Open current review</button>' +
'<button class="draft-copy" data-draft-index="' + index + '" type="button">Copy feedback</button>' +
'<button class="draft-discard" data-draft-index="' + index + '" type="button">Discard queued review</button>' :
item.kind === 'authored-outbox' && closureOutbox ?
'<button class="draft-resume" data-draft-index="' + index + '" type="button">Open issue</button>' +
'<button class="draft-authorize" data-draft-index="' + index + '" type="button">Authorize &amp; close</button>' +
'<button class="draft-discard" data-draft-index="' + index + '" type="button">Discard</button>' :
item.kind === 'authored-outbox' && !reviewOutbox ?
'<button class="draft-resume" data-draft-index="' + index + '" type="button">' +
(closureOutbox ? 'Open issue' : 'Open message') + '</button>' +
'<button class="draft-resume" data-draft-index="' + index + '" type="button">Open message</button>' +
'<button class="draft-send" data-draft-index="' + index + '" type="button">' + sendLabel + '</button>' +
'<button class="draft-discard" data-draft-index="' + index + '" type="button">Discard</button>' :
reviewOutbox ?
@@ -1676,7 +1679,8 @@
const state = (isOutbox || isUnfiled) ?
'<span class="pill">' + (item.quarantined ? 'Identity protected' :
(isUnfiled ? 'Needs filing' : item.status === 'completion' ? 'Created · ready to start' :
item.status === 'attention' ? 'Needs attention' : item.status === 'sending' ? 'Sending' : 'Queued for sync')) + '</span>' +
item.status === 'attention' ? 'Needs attention' : item.status === 'sending' ? 'Sending' :
item.status === 'authorization' ? 'Awaiting authorization' : 'Queued for sync')) + '</span>' +
(item.ownership ? '<div class="small">' + escapeHtml(item.ownership) + '</div>' : '') : '';
const attempt = item.last_attempt_error ? '<span class="delivery-attempt small">Last attempt ' +
escapeHtml(fmt(item.last_attempt_at)) + ' · ' + escapeHtml(item.last_attempt_error) + '</span>' : '';
@@ -1691,7 +1695,8 @@
'<div><h3 id="delivery-center-title">Delivery center</h3>' +
'<p class="small">Waiting <strong data-delivery-count="waiting">' + deliveryCenter.counts.waiting + '</strong> · ' +
'Sending <strong data-delivery-count="sending">' + deliveryCenter.counts.sending + '</strong> · ' +
'Needs attention <strong data-delivery-count="attention">' + deliveryCenter.counts.attention + '</strong></p></div>' +
'Needs attention <strong data-delivery-count="attention">' + deliveryCenter.counts.attention + '</strong> · ' +
'Authorize <strong data-delivery-count="authorization">' + deliveryCenter.counts.authorization + '</strong></p></div>' +
'<button id="retry-waiting-deliveries" type="button"' +
(deliveryCenter.retryable.length && activeFlushLogin ? '' : ' disabled') + '>Retry waiting</button></section>';
const deliveryCards = deliveryCenter.deliveries.length ? deliveryCenter.deliveries.map(renderDraftCard).join('') :
@@ -1769,6 +1774,18 @@
else applyOutboxResult(await issueOutbox.retry(item.outbox_id, activeFlushLogin));
});
});
list.querySelectorAll('.draft-authorize').forEach(button => {
button.addEventListener('click', async () => {
const item = lastDrafts[Number(button.dataset.draftIndex)];
if (!item?.outbox_id || !activeFlushLogin) return;
button.disabled = true;
qs('#my-work-action-status').textContent = 'Fresh authorization required for this exact issue.';
const result = await authoredOutbox.retry(item.outbox_id, activeFlushLogin);
applyAuthoredOutboxResult(result);
qs('#my-work-action-status').textContent = result.confirmed?.length ?
'Issue closed and queued intent cleared.' : 'Issue closure was not confirmed. The queued intent is still safe.';
});
});
list.querySelectorAll('.draft-copy').forEach(button => {
button.addEventListener('click', async () => {
const item = lastDrafts[Number(button.dataset.draftIndex)];
+8 -5
View File
@@ -160,10 +160,12 @@ function createDraftInbox({ storage, getCurrentLogin = () => '', now = () => Dat
outbox_id: item.id,
outbox_kind: item.kind,
kind: 'authored-outbox',
status: item.status === 'attention' ? 'attention' : (item.status === 'sending' ? 'sending' : 'queued'),
status: item.status === 'attention' ? 'attention' : (item.status === 'sending' ? 'sending' :
(isClosure ? 'authorization' : 'queued')),
label: item.deliveryState === 'uncertain' ? 'Verify delivery' :
(item.status === 'attention' ? (isClosure ? 'Issue closure needs attention' : 'Needs attention') :
(isReview ? 'Queued review' : (isClosure ? 'Queued issue closure' : 'Queued message'))),
(isReview ? 'Queued review' : (isClosure ? 'Awaiting authorization' : 'Queued message'))),
authorization_required: isClosure,
delivery_state: item.deliveryState,
repository: isUpdate ? '' : item.repository,
title: target,
@@ -237,12 +239,13 @@ function createDraftInbox({ storage, getCurrentLogin = () => '', now = () => Dat
const deliveries = items.filter(item => item.kind === 'issue-outbox' || item.kind === 'authored-outbox');
const drafts = items.filter(item => item.kind !== 'issue-outbox' && item.kind !== 'authored-outbox');
const counts = deliveries.reduce((summary, item) => {
const state = item.status === 'sending' ? 'sending' : (item.status === 'attention' ? 'attention' : 'waiting');
const state = item.status === 'sending' ? 'sending' : (item.status === 'attention' ? 'attention' :
(item.status === 'authorization' ? 'authorization' : 'waiting'));
summary[state] += 1;
return summary;
}, { waiting:0, sending:0, attention:0 });
}, { waiting:0, sending:0, attention:0, authorization:0 });
const retryable = deliveries.filter(item =>
item.status === 'queued' && !item.quarantined && item.delivery_state !== 'uncertain'
item.status === 'queued' && !item.authorization_required && !item.quarantined && item.delivery_state !== 'uncertain'
);
return { drafts, deliveries, counts, retryable };
}
+1 -1
View File
@@ -1,6 +1,6 @@
const BASE = new URL('./', self.location.href).pathname;
importScripts(BASE + 'static/background-issue-sync.js');
const CACHE = 'stackchain-dashboard-shell-v81';
const CACHE = 'stackchain-dashboard-shell-v82';
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;