stackchain-dashboard/frontend/drafts.js
timmy 34664edb8b
All checks were successful
CI / lint (pull_request) Successful in 40s
CI / build-release (pull_request) Successful in 4s
CI / release-candidate (pull_request) Has been skipped
feat: keep create and start durable (#389)
2026-08-09 09:41:10 +00:00

223 lines
9.3 KiB
JavaScript

function createDraftInbox({ storage, getCurrentLogin = () => '', now = () => Date.now() }) {
const indexKey = 'stackchain.draft-index.v1';
function readIndex() {
try {
const value = JSON.parse(storage?.getItem(indexKey) || '{}');
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
} catch (_error) { return {}; }
}
function writeIndex(index) {
try { storage?.setItem(indexKey, JSON.stringify(index)); }
catch (_error) { /* Draft discovery remains available without metadata. */ }
}
function keys() {
try {
return Array.from({ length: Number(storage?.length || 0) }, (_, index) => storage.key(index))
.filter(Boolean);
} catch (_error) { return []; }
}
function textPreview(value) {
return String(value || '').replace(/\s+/g, ' ').trim().slice(0, 180);
}
function parseTarget(value) {
const match = String(value || '').match(/^(.+)#(\d+)$/);
return match ? { repository: match[1], number: Number(match[2]) } : null;
}
function parse(key, raw) {
let match;
if (key === 'stackchain.issue-capture.v1') {
try {
const value = JSON.parse(raw);
const preview = textPreview([value?.title, value?.body].filter(Boolean).join(' — '));
if (!preview && !value?.repository) return null;
return {
kind: 'new-issue', label: 'New issue', repository: String(value?.repository || ''),
title: textPreview(value?.title) || 'Untitled new issue', preview,
};
} catch (_error) { return null; }
}
match = key.match(/^stackchain\.(issue|pull)-comment\.v1:(.+#\d+)$/);
if (match) {
const target = parseTarget(match[2]);
const preview = textPreview(raw);
if (!target || !preview) return null;
const routeKind = match[1] === 'issue' ? 'issue' : 'pull';
return {
kind: routeKind + '-comment', label: routeKind === 'issue' ? 'Issue comment' : 'PR comment',
...target, title: target.repository + '#' + target.number, preview,
route: { kind: routeKind, ...target },
};
}
match = key.match(/^stackchain\.issue-content\.v1:(.+#\d+)$/);
if (match) {
const target = parseTarget(match[1]);
try {
const value = JSON.parse(raw);
const preview = textPreview([value?.title, value?.body].filter(Boolean).join(' — '));
if (!target || !preview || typeof value?.expectedUpdatedAt !== 'string') return null;
return {
kind: 'issue-edit', label: 'Issue edit', ...target,
title: target.repository + '#' + target.number, preview,
route: { kind: 'issue', ...target },
};
} catch (_error) { return null; }
}
match = key.match(/^stackchain\.update-reply\.v1\.(\d+)$/);
if (match) {
const preview = textPreview(raw);
if (!preview) return null;
const notificationId = Number(match[1]);
return {
kind: 'update-reply', label: 'Update reply', notification_id: notificationId,
title: 'Update #' + notificationId, preview,
route: { kind: 'update', notification_id: notificationId },
};
}
match = key.match(/^stackchain\.review-draft\.v1:(.+#\d+)@([^:]+)$/);
if (match) {
const target = parseTarget(match[1]);
try {
const value = JSON.parse(raw);
const notes = Object.values(value?.notes || {});
const comments = Array.isArray(value?.comments) ? value.comments.map(item => item?.body) : [];
const preview = textPreview([value?.summary, ...notes, ...comments].filter(Boolean).join(' — '));
if (!target || !preview) return null;
return {
kind: 'review', label: 'PR review', ...target, head_sha: match[2],
title: target.repository + '#' + target.number, preview,
route: { kind: 'review', ...target },
};
} catch (_error) { return null; }
}
return null;
}
function parseOutbox(raw) {
try {
const record = JSON.parse(raw);
if (![1, 2, 3].includes(record?.version) || !Array.isArray(record.items)) return [];
const currentLogin = String(getCurrentLogin() || '').trim();
return record.items.filter(item =>
item && typeof item.id === 'string' && typeof item.repository === 'string' && typeof item.title === 'string'
).map(item => {
const ownerLogin = String(item.ownerLogin || '').trim();
const quarantined = !ownerLogin || !currentLogin || ownerLogin !== currentLogin;
return {
id: 'stackchain.issue-outbox.v1:' + item.id,
outbox_id: item.id,
kind: 'issue-outbox',
status: item.status === 'completion' ? 'completion' : (item.status === 'attention' ? 'attention' : 'queued'),
label: item.deliveryState === 'uncertain' ? 'Verify delivery' :
(item.status === 'completion' ? 'Created · ready to start' :
(item.status === 'attention' ? 'Needs attention' : 'Queued issue')),
continuation: item.status === 'completion' && item.completionIntent === 'create-and-start',
delivery_state: item.deliveryState,
repository: item.repository,
title: textPreview(item.title) || 'Untitled queued issue',
preview: textPreview([item.title, item.error || item.body].filter(Boolean).join(' — ')),
copy_text: [item.title, item.body].filter(Boolean).join('\n\n'),
quarantined,
ownership: quarantined ? 'Queued by ' + (ownerLogin || 'an unknown account') +
(currentLogin ? ' — current account is ' + currentLogin : ' — account confirmation unavailable') : '',
updated_at: Number(item.queuedAt || 0),
};
});
} catch (_error) { return []; }
}
function parseAuthoredOutbox(raw) {
try {
const record = JSON.parse(raw);
if (![1, 2].includes(record?.version) || !Array.isArray(record.items)) return [];
const currentLogin = String(getCurrentLogin() || '').trim();
return record.items.filter(item => item && typeof item.id === 'string' && typeof item.body === 'string')
.map(item => {
const isUpdate = item.kind === 'update-reply';
const routeKind = item.kind === 'pull-comment' ? 'pull' : 'issue';
const target = isUpdate ? 'Update #' + item.notificationId : item.repository + '#' + item.number;
const ownerLogin = String(item.ownerLogin || '').trim();
const quarantined = !ownerLogin || !currentLogin || ownerLogin !== currentLogin;
return {
id: 'stackchain.authored-outbox.v1:' + item.id,
outbox_id: item.id,
kind: 'authored-outbox',
status: item.status === 'attention' ? 'attention' : 'queued',
label: item.deliveryState === 'uncertain' ? 'Verify delivery' :
(item.status === 'attention' ? 'Needs attention' : 'Queued message'),
delivery_state: item.deliveryState,
repository: isUpdate ? '' : item.repository,
title: target,
preview: textPreview([item.error, item.body].filter(Boolean).join(' — ')),
copy_text: item.body,
quarantined,
ownership: quarantined ? 'Queued by ' + (ownerLogin || 'an unknown account') +
(currentLogin ? ' — current account is ' + currentLogin : ' — account confirmation unavailable') : '',
updated_at: Number(item.queuedAt || 0),
route: isUpdate ? { kind:'update', notification_id:item.notificationId } :
{ kind:routeKind, repository:item.repository, number:item.number },
};
});
} catch (_error) { return []; }
}
function list() {
const index = readIndex();
const seen = new Set();
const drafts = [];
keys().forEach(key => {
if (key === indexKey || key.endsWith(':operation')) return;
let raw;
try { raw = storage.getItem(key); }
catch (_error) { return; }
if (key === 'stackchain.issue-outbox.v1') {
parseOutbox(raw).forEach(item => drafts.push(item));
seen.add(key);
return;
}
if (key === 'stackchain.authored-outbox.v1') {
parseAuthoredOutbox(raw).forEach(item => drafts.push(item));
seen.add(key);
return;
}
const parsed = parse(key, raw);
if (!parsed) return;
seen.add(key);
const fingerprint = String(raw);
const previous = index[key];
if (!previous || previous.fingerprint !== fingerprint) {
index[key] = { fingerprint, updated_at: Number(now()) };
}
drafts.push({ id: key, ...parsed, updated_at: index[key].updated_at });
});
Object.keys(index).forEach(key => { if (!seen.has(key)) delete index[key]; });
writeIndex(index);
return drafts.sort((left, right) =>
Number(right.updated_at || 0) - Number(left.updated_at || 0) || left.id.localeCompare(right.id)
);
}
function discard(id) {
if (typeof id !== 'string' || !parse(id, (() => {
try { return storage?.getItem(id); } catch (_error) { return null; }
})())) return false;
try {
storage?.removeItem(id);
storage?.removeItem(id + ':operation');
const index = readIndex();
delete index[id];
writeIndex(index);
return true;
} catch (_error) { return false; }
}
return { list, discard };
}
if (typeof module !== 'undefined' && module.exports) module.exports = createDraftInbox;