dashboard: queued reminders surface
new 'qu3u3d r3m1nd3rs' section between approvals and operator
inbox. lists every pending reminder with agent, due-relative
timestamp, body, payload path (path-linkified), and a cancel
button. drives off a new /api/reminders endpoint and a
POST /cancel-reminder/{id} that hard-deletes the row.
failure surface (last_error / attempt_count + retry) deferred —
needs a sqlite migration; tracked in TODO.md.
This commit is contained in:
parent
cb71a07300
commit
1db6b8ffed
6 changed files with 183 additions and 4 deletions
|
|
@ -46,6 +46,19 @@ const EVENT_CHANNEL: usize = 256;
|
|||
/// self-documenting.
|
||||
pub type DueReminder = (String, i64, String, Option<String>);
|
||||
|
||||
/// Row shape for [`Broker::list_pending_reminders`], shipped on the
|
||||
/// dashboard `/api/reminders` response.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct PendingReminder {
|
||||
pub id: i64,
|
||||
pub agent: String,
|
||||
pub message: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub file_path: Option<String>,
|
||||
pub due_at: i64,
|
||||
pub created_at: i64,
|
||||
}
|
||||
|
||||
/// Intra-process broker event. `recv_blocking` listens on the same
|
||||
/// channel as the dashboard forwarder; the forwarder re-emits each
|
||||
/// event as a `DashboardEvent` with a freshly-stamped seq from the
|
||||
|
|
@ -286,6 +299,44 @@ impl Broker {
|
|||
Ok(id)
|
||||
}
|
||||
|
||||
/// Every reminder still pending delivery, newest-first. Used by the
|
||||
/// dashboard's reminders pane so the operator can see what's queued
|
||||
/// + cancel rows that are no longer wanted.
|
||||
pub fn list_pending_reminders(&self) -> Result<Vec<PendingReminder>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, agent, message, file_path, due_at, created_at \
|
||||
FROM reminders \
|
||||
WHERE sent_at IS NULL \
|
||||
ORDER BY due_at ASC",
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok(PendingReminder {
|
||||
id: row.get(0)?,
|
||||
agent: row.get(1)?,
|
||||
message: row.get(2)?,
|
||||
file_path: row.get(3)?,
|
||||
due_at: row.get(4)?,
|
||||
created_at: row.get(5)?,
|
||||
})
|
||||
})?;
|
||||
rows.collect::<rusqlite::Result<Vec<_>>>()
|
||||
.context("list pending reminders")
|
||||
}
|
||||
|
||||
/// Delete a reminder by id. Returns the number of rows removed (0
|
||||
/// when the id never existed or was already delivered). Hard
|
||||
/// delete rather than soft so the row doesn't linger and confuse a
|
||||
/// re-creation under the same id.
|
||||
pub fn cancel_reminder(&self, id: i64) -> Result<usize> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let n = conn.execute(
|
||||
"DELETE FROM reminders WHERE id = ?1 AND sent_at IS NULL",
|
||||
params![id],
|
||||
)?;
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
/// Get up to `limit` due reminders across all agents in a single query.
|
||||
/// Returns `(agent, id, message, file_path)` tuples. Pass a small limit
|
||||
/// (e.g. 100) so a burst of overdue reminders doesn't flood the broker
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue