reminders: persist + surface delivery failures
Broker schema gains attempt_count INTEGER + last_error TEXT
columns via idempotent ALTER TABLE migration (pragma-probed so
fresh + existing dbs converge). reminder_scheduler::tick calls
record_reminder_failure on every deliver_reminder error,
bumping the counter + stashing the message. get_due_reminders
filters out rows where attempt_count >= MAX_REMINDER_ATTEMPTS
(5) so the scheduler stops retrying a stuck row until the
operator intervenes.
new POST /retry-reminder/{id} → reset_reminder_failure clears
the counters; next 5s tick re-attempts. cancel-reminder
unchanged (hard-delete).
dashboard renders failed rows with a red left rule, the error
text inline, and a ⚠ N failed badge. ↻ R3TRY button appears
when attempt_count > 0 — sits next to ✗ C4NC3L in a small
actions row below the body.
This commit is contained in:
parent
d395bdc945
commit
978a3cf391
5 changed files with 173 additions and 8 deletions
|
|
@ -1349,7 +1349,8 @@
|
|||
}
|
||||
const ul = el('ul', { class: 'reminders' });
|
||||
for (const r of rows) {
|
||||
const li = el('li', { class: 'reminder-row' });
|
||||
const failed = (r.attempt_count || 0) > 0;
|
||||
const li = el('li', { class: 'reminder-row' + (failed ? ' reminder-failed' : '') });
|
||||
const dueIn = r.due_at - Math.floor(Date.now() / 1000);
|
||||
const dueLabel = dueIn <= 0
|
||||
? `overdue ${fmtAgo(r.due_at)}`
|
||||
|
|
@ -1364,19 +1365,45 @@
|
|||
head.append(' ', el('span', { class: 'meta' }, '· payload → '));
|
||||
appendLinkified(head, r.file_path);
|
||||
}
|
||||
if (failed) {
|
||||
head.append(' ', el('span',
|
||||
{
|
||||
class: 'badge badge-warn',
|
||||
title: 'consecutive failed delivery attempts (capped at 5; over the cap the scheduler stops retrying until you click R3TRY or cancel)',
|
||||
},
|
||||
`⚠ ${r.attempt_count} failed`));
|
||||
}
|
||||
const body = el('div', { class: 'reminder-body' });
|
||||
const previews = appendLinkified(body, r.message);
|
||||
li.append(head, body);
|
||||
for (const d of previews) li.appendChild(d);
|
||||
// Cancel form omits `data-no-refresh` — the resulting refreshState
|
||||
// re-fires refreshReminders so the row drops on its own.
|
||||
if (r.last_error) {
|
||||
li.append(el('div', { class: 'reminder-error' },
|
||||
el('span', { class: 'msg-sep' }, 'error: '),
|
||||
r.last_error,
|
||||
));
|
||||
}
|
||||
const actions = el('div', { class: 'reminder-actions' });
|
||||
if (failed) {
|
||||
// Retry resets the failure counters so the scheduler picks
|
||||
// the row up again on its next 5s tick. No data-no-refresh
|
||||
// — the resulting refreshState re-fires refreshReminders.
|
||||
const retryForm = el('form', {
|
||||
method: 'POST', action: '/retry-reminder/' + r.id,
|
||||
class: 'inline', 'data-async': '',
|
||||
});
|
||||
retryForm.append(el('button',
|
||||
{ type: 'submit', class: 'btn btn-restart' }, '↻ R3TRY'));
|
||||
actions.append(retryForm);
|
||||
}
|
||||
const cancelForm = el('form', {
|
||||
method: 'POST', action: '/cancel-reminder/' + r.id,
|
||||
class: 'inline', 'data-async': '',
|
||||
'data-confirm': `cancel reminder ${r.id} for ${r.agent}? this drops the queued delivery; no undo.`,
|
||||
});
|
||||
cancelForm.append(el('button', { type: 'submit', class: 'btn btn-deny' }, '✗ C4NC3L'));
|
||||
li.append(cancelForm);
|
||||
actions.append(cancelForm);
|
||||
li.append(actions);
|
||||
ul.append(li);
|
||||
}
|
||||
root.append(ul);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue