{ "description": "Reminders app event vectors: ensures remind fold handlers produce consistent state. Source of truth: web/static/apps/remind.js", "app": "remind", "source": "web/static/apps/remind.js", "state_shape": { "items": [{ "id": "string", "text": "string", "when": "number (epoch ms, due time)", "ts": "number (epoch ms, add time)", "sid": "string (server schedule id, empty if offline)", "fired": "boolean", "firedAt": "number (epoch ms)" }] }, "vectors": [ { "name": "remind_add", "description": "Add a reminder with a future due time (sid = server schedule id, empty if offline).", "event": { "type": "remind.add", "id": "remind-1", "text": "call plumber", "when": 1700003600000, "ts": 1700000000000, "sid": "sch-abc123" }, "expected_state_update": { "items": [ { "id": "remind-1", "text": "call plumber", "when": 1700003600000, "ts": 1700000000000, "sid": "sch-abc123", "fired": false, "firedAt": 0 } ] }, "note": "Items are sorted by `when` (soonest first). `sid` is the server's scheduled-message id; empty string if scheduling failed (client falls back to best-effort firing). `when` is absolute epoch ms. No chat projection (reminders are silent in chat)." }, { "name": "remind_fire", "description": "A scheduled reminder fires (delivered at the due time, either by server schedule or client best-effort).", "event": { "type": "remind.fire", "id": "remind-1", "notice": { "title": "⏰ Reminder", "body": "call plumber" }, "ts": 1700003600000 }, "initial_state": { "items": [ { "id": "remind-1", "text": "call plumber", "when": 1700003600000, "ts": 1700000000000, "sid": "sch-abc123", "fired": false, "firedAt": 0 } ] }, "expected_state_update": { "items": [ { "id": "remind-1", "text": "call plumber", "when": 1700003600000, "ts": 1700000000000, "sid": "sch-abc123", "fired": true, "firedAt": 1700003600000 } ] }, "expected_chat_projection": { "text": "⏰ Reminder: call plumber", "action": false }, "note": "`notice` has the title + body shown by push/toast (the text the user sees on alert). `firedAt` is the server delivery time (for grace-period animation, same 750ms linger-then-tuck as todo). When this event lands in the chat thread, it's surfaced as a message (not an action line) with the title+body from the notice." }, { "name": "remind_done", "description": "Dismiss a reminder (removes it from the list; if unfired, also cancels the server schedule via the sid).", "event": { "type": "remind.done", "id": "remind-1" }, "initial_state": { "items": [ { "id": "remind-1", "text": "call plumber", "when": 1700003600000, "ts": 1700000000000, "sid": "sch-abc123", "fired": false, "firedAt": 0 } ] }, "expected_state_update": { "items": [] }, "note": "If `fired=false` and `sid` is set, the client sends an unschedule request to cancel the server's scheduled remind.fire. If the cancel races the delivery, the late remind.fire folds onto a missing id (a no-op)." }, { "name": "remind_grace_period_partition", "description": "Grace period: active (unfired or recently fired within 750ms) vs. archived (fired + >750ms ago).", "note": "Like todo, this is derived in render from Date.now() and firedAt, never by a timer. Native should partition: unfired items + recently-fired (firedAt > now - 750ms) stay visible; older fired reminders move to 'Previous reminders' section.", "constants": { "REMIND_GRACE_MS": 750 } }, { "name": "remind_multiple_sorted", "description": "Multiple reminders are kept sorted by `when` (soonest first), regardless of add order.", "event_sequence": [ { "type": "remind.add", "id": "remind-1", "text": "late one", "when": 1700010000000, "ts": 1700000000000, "sid": "sch-abc123" }, { "type": "remind.add", "id": "remind-2", "text": "early one", "when": 1700003600000, "ts": 1700000100000, "sid": "sch-def456" } ], "expected_final_state": { "items": [ { "id": "remind-2", "text": "early one", "when": 1700003600000, "ts": 1700000100000, "sid": "sch-def456", "fired": false, "firedAt": 0 }, { "id": "remind-1", "text": "late one", "when": 1700010000000, "ts": 1700000000000, "sid": "sch-abc123", "fired": false, "firedAt": 0 } ] }, "note": "The list is re-sorted on every remind.add, so the order is always soonest-first regardless of user input order." } ] }