feat(scheduled): recurring scheduled messages (LC-485) #465

Merged
longjacksonle merged 4 commits from feat/LC-485-recurring-scheduled into main 2026-06-28 00:23:27 +02:00

What

Recurring scheduled messages (LC-485) - schedule a message to repeat daily / every weekday / weekly. Drives standups, reminders, digests. Built on the existing one-shot scheduled-message pipeline (LC-62).

How

  • scheduled_messages.repeat (migration 0070): none (default) / daily / weekly / weekdays.
  • scheduled::recurrence: computes the next occurrence in the stored UTC format, advancing past now so a dispatcher downtime backlog catches up to the next future slot instead of re-firing the past; weekdays skips Sat/Sun. Unit-tested.
  • Re-enqueue on delivery: the dispatcher inserts the next occurrence inside the same claim transaction that marks the current row delivered - crash-safe, and since the row is already claimed a re-run can't double-enqueue. Each fire is its own auditable row; cancelling the pending row stops the series. Next occurrences are plain top-level posts (attachment/parent/quote are one-shot). A dropped occurrence ends the series (avoids FK issues + runaway retries on a permanently-broken target).
  • UI: a Repeat select in the schedule modal (threaded through CreateForm -> insert_scheduled_with_repeat, server-validated) and a "Repeats ..." badge on the /scheduled pending list. i18n en + es.

Retention / rate-limit

Delivered recurring messages are ordinary messages rows, so retention sweeps them automatically. Per the existing one-shot design, the deliver path does not re-check the per-minute rate limit (rate-limit is enforced at schedule time only); recurring occurrences inherit that posture.

Tests

Dispatcher: a delivered recurring row enqueues exactly one future-dated next occurrence (same repeat kind), the next tick is a no-op, and a one-shot leaves nothing pending. Routes: the repeat field stores, defaults to none, invalid values 400. Plus recurrence unit tests. just check, just test, just test-saas all green.

Verify

just verify builds the release binary clean; boot fails only on the missing SSO env (environmental, as in prior PRs).

## What Recurring scheduled messages (LC-485) - schedule a message to repeat daily / every weekday / weekly. Drives standups, reminders, digests. Built on the existing one-shot scheduled-message pipeline (LC-62). ## How - **`scheduled_messages.repeat`** (migration 0070): `none` (default) / `daily` / `weekly` / `weekdays`. - **`scheduled::recurrence`**: computes the next occurrence in the stored UTC format, advancing past now so a dispatcher downtime backlog catches up to the next future slot instead of re-firing the past; `weekdays` skips Sat/Sun. Unit-tested. - **Re-enqueue on delivery**: the dispatcher inserts the next occurrence **inside the same claim transaction** that marks the current row delivered - crash-safe, and since the row is already claimed a re-run can't double-enqueue. Each fire is its own auditable row; cancelling the pending row stops the series. Next occurrences are plain top-level posts (attachment/parent/quote are one-shot). A dropped occurrence ends the series (avoids FK issues + runaway retries on a permanently-broken target). - **UI**: a Repeat select in the schedule modal (threaded through `CreateForm` -> `insert_scheduled_with_repeat`, server-validated) and a "Repeats ..." badge on the `/scheduled` pending list. i18n en + es. ## Retention / rate-limit Delivered recurring messages are ordinary `messages` rows, so retention sweeps them automatically. Per the existing one-shot design, the deliver path does not re-check the per-minute rate limit (rate-limit is enforced at schedule time only); recurring occurrences inherit that posture. ## Tests Dispatcher: a delivered recurring row enqueues exactly one future-dated next occurrence (same repeat kind), the next tick is a no-op, and a one-shot leaves nothing pending. Routes: the repeat field stores, defaults to none, invalid values 400. Plus recurrence unit tests. `just check`, `just test`, `just test-saas` all green. ## Verify `just verify` builds the release binary clean; boot fails only on the missing SSO env (environmental, as in prior PRs).
Migration 0070 adds scheduled_messages.repeat (none/daily/weekly/weekdays, default none). New scheduled::recurrence computes the next occurrence in the stored UTC format, advancing past now so a downtime backlog catches up to the next future slot rather than re-firing the past. weekdays skips Sat/Sun. Unit-tested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ScheduledRow carries repeat; insert_scheduled_with_repeat sets it (the 7-arg insert_scheduled stays a one-shot wrapper). On the delivered path the dispatcher inserts the next occurrence inside the same claim transaction (crash-safe; the row is already claimed so a re-run can't double-enqueue). Next occurrences are plain top-level posts (attachment/parent/quote are one-shot). Dropped rows end the series, avoiding FK issues and runaway retries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The schedule modal gains a Repeat select (Does not repeat / daily / every weekday / weekly), threaded through CreateForm -> insert_scheduled_with_repeat with server-side validation. The /scheduled pending list shows a "Repeats ..." badge per recurring row. i18n in en + es.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test(scheduled): cover recurrence re-enqueue + repeat form field (LC-485)
Some checks failed
check-secrets / Nosey parker (push) Successful in 4s
check-secrets / TruffleHog (push) Successful in 6s
check-secrets / Kingfisher (push) Failing after 6s
check-secrets / TruffleHog (pull_request) Successful in 8s
check-secrets / Nosey parker (pull_request) Successful in 9s
check-secrets / Kingfisher (pull_request) Failing after 11s
Check / clippy + fmt + tests (pull_request) Successful in 5m42s
Create release / Create release from merged PR (pull_request) Has been skipped
bba1217b3f
Dispatcher: a delivered recurring row enqueues exactly one future-dated next occurrence with the same repeat kind, and the next tick is a no-op; a one-shot leaves nothing pending. Routes: the repeat field is stored, defaults to none, and an unknown value is rejected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle scheduled this pull request to auto merge when all checks succeed 2026-06-28 00:23:20 +02:00
longjacksonle deleted branch feat/LC-485-recurring-scheduled 2026-06-28 00:23:27 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!465
No description provided.