yiekheng 48cae84919 feat(recurrence): Yearly tab — month grid + day grid, both multi-select
Yearly was a single Month dropdown + a Day number input — one Month and
one Day per rule. That meant "every quarter on the 1st" needed four
separate schedule rows.

Now Yearly mirrors Monthly's grid pattern but with two grids:

  Months   [Jan][Feb][Mar][Apr][May][Jun]
           [Jul][Aug][Sep][Oct][Nov][Dec]

  Days     [ 1][ 2][ 3]...[31]   (7×5 grid)

Both grids are multi-select. Cron output uses the comma-list form on
both DOM and month positions:

  months: [1,4,7,10] + days: [1]   →   "0 9 1 1,4,7,10 *"
  months: [12]       + days: [24,25,31] → "0 9 24,25,31 12 *"

The cron field is a Cartesian product — every selected day fires in
every selected month. So "every quarter on the 1st" is now one rule.

Round-trip: parser accepts comma-lists for both DOM and month, with
single-element shapes (the old "0 9 13 5 *") still loading fine.

Migration of saved data: old yearly rules with one DOM + one month
parse into monthDays=[X], months=[Y] — identical visual selection in
the new grid, identical cron output. No DB changes needed.

Renamed `Draft.month` to `Draft.months: number[]`. The "Single
day-of-month for yearly" field is gone — yearly now reads
`monthDays` (same as monthly).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:28:42 +08:00
..