fix(#2650,#2649): drop reminders tab + pending_reminders chip

hive-c0re's broker reminder store and /api/reminders endpoint were
removed in PR #2644 (reminders migrated to in-container sqlite store).
The dashboard had a QU3U3D R3M1ND3RS section on the schedules tab backed
by that endpoint, and a per-agent badge driven by pending_reminders
(always 0 after the migration).

Remove both. Per-agent reminders now surface through get_loose_ends /
the todos pill on the agent page, consistent with the todos migration.

- frontend/packages/dashboard/src/schedules.js: drop refreshReminders,
  renderReminders, applyRemindersChanged; drop appendLinkified import
  (no longer used); update module comment.
- frontend/packages/dashboard/src/tabs.js: drop applyRemindersChanged
  and refreshReminders imports; remove reminders-section from managed
  list; remove refreshReminders call site; drop reminders_changed from
  SSE dispatch; simplify countdown ticker (reminder-due gone).
- frontend/packages/dashboard/src/dashboard.html: remove QU3U3D
  R3M1ND3RS section.
- frontend/packages/dashboard/src/dashboard.css: remove reminder list +
  row CSS.
- frontend/packages/dashboard/src/common.css: remove .badge-reminder.
- frontend/packages/dashboard/src/swarm.js: remove pending_reminders
  from fingerprint key and badge render.
This commit is contained in:
iris 2026-07-23 00:17:41 +02:00
commit fab2fdcc0a
6 changed files with 21 additions and 180 deletions

View file

@ -77,10 +77,6 @@ code {
color: var(--muted); border-color: var(--purple-dim);
background: color-mix(in srgb, var(--muted) 8%, transparent);
}
.badge-reminder {
color: var(--cyan); border-color: var(--cyan);
text-shadow: 0 0 6px color-mix(in srgb, var(--cyan) 40%, transparent);
}
.badge-loose-ends {
color: var(--purple); border-color: var(--purple);
text-shadow: 0 0 6px color-mix(in srgb, var(--purple) 40%, transparent);

View file

@ -675,42 +675,6 @@ summary:hover { color: var(--purple); }
padding: 0.6em 0.9em;
animation: questions-pulse 2.4s ease-in-out infinite;
}
.reminders {
list-style: none;
padding: 0;
margin: 0;
}
.reminder-row {
padding: 0.4em 0;
border-bottom: 1px solid var(--border);
}
.reminder-row:last-child { border-bottom: 0; }
.reminder-head { font-size: 0.9em; }
.reminder-body {
color: var(--fg);
white-space: pre-wrap;
word-break: break-word;
margin: 0.3em 0;
}
.reminder-row.reminder-failed {
border-left: 2px solid var(--red);
padding-left: 0.5em;
}
.reminder-error {
color: var(--red);
background: color-mix(in srgb, var(--red) 6%, transparent);
border: 1px solid color-mix(in srgb, var(--red) 25%, transparent);
padding: 0.3em 0.5em;
font-size: 0.85em;
white-space: pre-wrap;
word-break: break-word;
margin: 0.2em 0;
}
.reminder-actions {
display: flex;
gap: 0.4em;
margin-top: 0.3em;
}
.questions-filters {
display: flex;

View file

@ -206,19 +206,6 @@
<p class="meta">loading…</p>
</div>
<!-- QU3U3D R3M1ND3RS: self-scheduled agent reminders. Lives
on this tab so the operator has one place for everything
that fires at a future time — operator-set schedules
above, agent-self reminders here. Backed by GET
/api/reminders; live updates via `reminders_changed` SSE;
refreshReminders() called from refreshState + tab
activation as safety net for disconnect windows. -->
<h2>◆ QU3U3D R3M1ND3RS ◆</h2>
<div class="divider">══════════════════════════════════════════════════════════════</div>
<p class="meta">reminders agents have queued for themselves but not yet delivered. cancel to drop a stuck or unwanted entry.</p>
<div id="reminders-section">
<p class="meta">loading…</p>
</div>
</section>
<!-- ST4TS: hive-wide turn-stats aggregate lives on its own page now

View file

@ -1,114 +1,24 @@
// Dashboard SCH3DUL3S tab — reminders + scheduled-prompts.
// Dashboard SCH3DUL3S tab — scheduled-prompts.
//
// Reminders (a separate sqlite table, not part of /api/state) and the
// operator's scheduled prompts: the lists, the inline create row, the
// per-row edit form, and the fire-now / cancel actions. Live updates
// arrive via the `reminders_changed` / `schedules_changed` dashboard
// events (wired into the entry's mutation dispatch table); the
// tab-activation re-fetch is the cold-load / reconnect recovery path.
// The operator's scheduled prompts: the list, the inline create row,
// the per-row edit form, and the fire-now / cancel actions. Live updates
// arrive via the `schedules_changed` dashboard event (wired into the
// entry's mutation dispatch table); the tab-activation re-fetch is the
// cold-load / reconnect recovery path.
//
// Owns its own module state (the schedules list + edit-in-progress
// tracking); reads the shared agent roster from state.js for the
// target-chip pickers. Render + format helpers come from util.js.
//
// Note: per-agent reminders moved to the in-container store in PR #2644
// and are no longer listed here — they surface via get_loose_ends /
// the todos pill on the agent page instead.
import { $, el, appendLinkified } from './common.js';
import { $, el } from './common.js';
import { themedConfirm, themedToast } from './modal.js';
import { paintAtomic, epochSec, fmtAgo, fmtDuration } from './util.js';
import { containersState } from './state.js';
import { asyncBtn } from '@hive/shared/forms.js';
export async function refreshReminders() {
const liveRoot = $('reminders-section');
if (!liveRoot) return;
try {
const resp = await fetch('/api/reminders');
if (!resp.ok) {
paintAtomic(liveRoot, (root) => {
root.append(el('p', { class: 'empty' }, 'reminders unavailable: http ' + resp.status));
});
return;
}
const rows = await resp.json();
renderReminders(rows);
} catch (err) {
paintAtomic(liveRoot, (root) => {
root.append(el('p', { class: 'empty' }, 'reminders fetch failed: ' + err));
});
}
}
function renderReminders(rows) {
const liveRoot = $('reminders-section');
if (!liveRoot) return;
paintAtomic(liveRoot, (root) => {
if (!rows.length) {
root.append(el('p', { class: 'empty' }, 'no queued reminders'));
return;
}
const ul = el('ul', { class: 'reminders' });
for (const r of rows) {
const failed = (r.attempt_count || 0) > 0;
const li = el('li', { class: 'reminder-row' + (failed ? ' reminder-failed' : '') });
const dueIn = epochSec(r.due_at) - Math.floor(Date.now() / 1000);
const dueLabel = dueIn <= 0
? `overdue ${fmtAgo(r.due_at)}`
: `in ${fmtDuration(dueIn)}`;
const head = el('div', { class: 'reminder-head' },
el('span', { class: 'agent' }, r.agent), ' ',
el('span', {
class: 'meta reminder-due',
title: new Date(r.due_at).toISOString(),
'data-due-at': String(epochSec(r.due_at)),
}, dueLabel),
' ',
el('span', { class: 'meta' }, `· id ${r.id}`),
);
if (r.file_path) {
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' });
appendLinkified(body, r.message);
li.append(head, body);
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: '/api/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: '/api/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'));
actions.append(cancelForm);
li.append(actions);
ul.append(li);
}
root.append(ul);
});
}
// ─── scheduled prompts ─────────────────────────────────────────────────
// Backend exposes `/api/schedules` (snapshot), `/api/schedules`
// (POST, operator-direct submit), `/api/schedules/{id}/cancel`
@ -1144,6 +1054,3 @@ export function applySchedulesChanged(ev) {
schedulesState = (ev.schedules || []).slice();
renderSchedulesList();
}
export function applyRemindersChanged(ev) {
renderReminders(ev.reminders || []);
}

View file

@ -459,7 +459,6 @@ function containerRowFingerprint(c, node, pending, opRunning, selected,
running: c.running,
needs_login: c.needs_login,
needs_update: c.needs_update,
pending_reminders: c.pending_reminders,
active_model: c.active_model,
port: c.port,
pending,
@ -672,14 +671,6 @@ function buildContainerLi(c, node, opts) {
c.active_model));
}
if (c.pending_reminders && c.pending_reminders > 0) {
head.append(el('span',
{
class: 'badge badge-reminder',
title: 'pending reminders queued for this agent — see the reminders section to view / cancel',
},
`${c.pending_reminders}`));
}
// Pending questions where this agent is the asker (awaiting an
// answer) or the target (owes a reply). Derived live from
// questionsState so the badge updates instantly on QuestionAdded /

View file

@ -28,8 +28,8 @@ import {
initPermissions,
} from './permissions.js';
import {
applySchedulesChanged, applyRemindersChanged,
refreshSchedules, refreshReminders, activeScheduleCount,
applySchedulesChanged,
refreshSchedules, activeScheduleCount,
} from './schedules.js';
import {
initCall,
@ -105,11 +105,10 @@ window.marked = marked;
// a request pending for an hour could still show "0s ago" without
// this ticker. Also flips `.stale` (amber highlight) at exactly 1h
// rather than only at the next re-render.
// Live countdown for reminder due-at labels and schedule next-fire
// cells. Both renderers stamp `data-due-at` on the element so this
// single ticker keeps them fresh without triggering a full re-render.
// `.reminder-due` -> "overdue X ago" / "in Y"
// `.sched-due` -> same pattern
// Live countdown for schedule next-fire cells. Renderers stamp
// `data-due-at` on `.sched-due` elements so this single ticker keeps
// them fresh without triggering a full re-render. Also keeps
// `.approval-ts` stale-highlight fresh at the 1h boundary.
setInterval(() => {
const now = Math.floor(Date.now() / 1000);
document.querySelectorAll('.approval-ts[data-requested-at]').forEach((node) => {
@ -119,13 +118,13 @@ window.marked = marked;
node.textContent = 'requested ' + fmtAgo(requestedAt);
node.classList.toggle('stale', ageSec >= 3600);
});
document.querySelectorAll('.reminder-due[data-due-at], .sched-due[data-due-at]').forEach((node) => {
document.querySelectorAll('.sched-due[data-due-at]').forEach((node) => {
const dueAt = Number(node.getAttribute('data-due-at'));
if (!Number.isFinite(dueAt)) return;
const dueIn = dueAt - now;
node.textContent = dueIn <= 0
? 'overdue ' + fmtAgo(dueAt)
: (node.classList.contains('reminder-due') ? 'in ' : '') + fmtDuration(dueIn);
: fmtDuration(dueIn);
});
}, 1000);
@ -139,7 +138,6 @@ window.marked = marked;
'questions-section',
'inbox-section',
'approvals-section',
'reminders-section',
'schedules-section',
'capabilities-section',
'tool-groups-section',
@ -248,7 +246,6 @@ window.marked = marked;
// #inbox-section element to render into.)
syncApprovalsFromSnapshot(s);
renderApprovals();
refreshReminders();
refreshSchedules();
restoreOpenDetails(openDetails);
notifyDeltas(s);
@ -310,7 +307,6 @@ window.marked = marked;
// rebuild_queue_changed stays: it refreshes the SW4RM badges.
rebuild_queue_changed: applyRebuildQueueChanged,
schedules_changed: applySchedulesChanged,
reminders_changed: applyRemindersChanged,
capabilities_changed: applyCapabilitiesChanged,
tool_groups_changed: applyToolGroupsChanged,
};
@ -370,8 +366,8 @@ window.marked = marked;
renderSelectionBar(Array.from(containersState.values()));
// Re-fetch on activation as a safety net: SSE covers live mutations,
// re-sync covers disconnect windows / approval-path inserts that
// don't yet emit. SCH3DUL3S also re-fetches reminders (same tab).
if (target === 'schedules') { refreshSchedules(); refreshReminders(); }
// don't yet emit.
if (target === 'schedules') { refreshSchedules(); }
// Permissions tables: SSE covers worker-applied changes
// (capabilities_changed / tool_groups_changed); re-fetch on
// activation as a safety net for any gap between SSE events and