fix(admin): make bulk delete retry safe + clarify copyable errors

This commit is contained in:
Young Lee
2026-02-06 15:13:43 -08:00
parent de7978f7bc
commit fe1fcda745
+44 -23
View File
@@ -1265,7 +1265,7 @@ app.get("/", async (c) => {
return confirm( return confirm(
'Delete ' + 'Delete ' +
selected + selected +
' selected feed(s)? This will also delete all emails inside those feeds.' + ' selected feed(s)? This disables the feeds immediately. Stored emails are cleaned up best-effort and may take a while.' +
extra, extra,
); );
} }
@@ -1376,32 +1376,53 @@ app.get("/", async (c) => {
updateFeedMatchCount(); updateFeedMatchCount();
updateFeedSelectionState(); updateFeedSelectionState();
// If a batch fails for some feeds, retry those one-by-one using the single delete endpoint. // If a batch fails for some feeds, retry those one-by-one using the bulk-delete
if (failedIds.length > 0) { // endpoint with a single id (keeps semantics consistent and avoids hiding active feeds).
if (failedIds.length > 0) {
if (toast && toast.update) { if (toast && toast.update) {
toast.update('Retrying ' + failedIds.length + ' failed feed(s) one-by-one...', { type: 'info' }); toast.update('Retrying ' + failedIds.length + ' failed feed(s) one-by-one...', { type: 'info' });
} }
const stillFailed = []; const stillFailed = [];
for (let j = 0; j < failedIds.length; j++) { for (let j = 0; j < failedIds.length; j++) {
const feedId = String(failedIds[j] || ''); const feedId = String(failedIds[j] || '');
if (!feedId) continue; if (!feedId) continue;
try { try {
const retryRes = await fetch('/admin/feeds/' + encodeURIComponent(feedId) + '/delete?view=table', { const retryRes = await fetch('/admin/feeds/bulk-delete', {
method: 'POST', method: 'POST',
headers: { 'Accept': 'application/json' }, headers: {
credentials: 'same-origin', 'Content-Type': 'application/json',
}); 'Accept': 'application/json',
if (window.parseJsonResponseOrThrow) { },
await window.parseJsonResponseOrThrow(retryRes, { prefix: 'Retry delete failed' }); credentials: 'same-origin',
} else if (!retryRes.ok) { body: JSON.stringify({ feedIds: [feedId] }),
throw new Error('Retry delete failed (HTTP ' + retryRes.status + ')'); });
}
removeFeedRowsById([feedId]); let retryData = {};
deletedTotal += 1; if (window.parseJsonResponseOrThrow) {
} catch (e) { retryData = await window.parseJsonResponseOrThrow(retryRes, { prefix: 'Retry delete failed' });
stillFailed.push(feedId); } else {
} retryData = await retryRes.json().catch(() => ({}));
if (!retryRes.ok) {
const message = retryData && retryData.error ? String(retryData.error) : ('Retry delete failed (HTTP ' + retryRes.status + ')');
throw new Error(message);
}
}
const retryDeleted = Array.isArray(retryData.deletedFeedIds) ? retryData.deletedFeedIds : [];
const retryFailed = Array.isArray(retryData.failedFeedIds) ? retryData.failedFeedIds : [];
if (retryDeleted.includes(feedId)) {
removeFeedRowsById([feedId]);
deletedTotal += 1;
} else if (retryFailed.includes(feedId)) {
stillFailed.push(feedId);
} else {
stillFailed.push(feedId);
}
} catch (e) {
stillFailed.push(feedId);
}
if (toast && toast.update) { if (toast && toast.update) {
toast.update('Retrying... (' + (j + 1) + ' of ' + failedIds.length + ')', { type: 'info' }); toast.update('Retrying... (' + (j + 1) + ' of ' + failedIds.length + ')', { type: 'info' });