Merge pull request #801 from otherwiseGG/main

feat: added UI support for ordinal weekdays
This commit is contained in:
Raj Nandan Sharma
2026-08-03 10:11:22 +05:30
committed by GitHub
2 changed files with 44 additions and 10 deletions
@@ -28,10 +28,11 @@ FREQ=frequency[;INTERVAL=n][;BYDAY=days][;BYMONTHDAY=day][;COUNT=n][;UNTIL=date]
- Default: 1
- Integer value: 2 = every other, 3 = every third, etc.
**BYDAY** (Optional, for WEEKLY)
**BYDAY** (Optional, for WEEKLY or MONTHLY with an ordinal prefix)
- Day codes: `MO`, `TU`, `WE`, `TH`, `FR`, `SA`, `SU`
- Multiple days: `MO,WE,FR`
- With MONTHLY, prefix a day code with an ordinal to mean "the Nth such weekday of the month": `1WE` (first Wednesday), `1SU` (first Sunday)
**BYMONTHDAY** (Optional, for MONTHLY)
@@ -240,6 +241,29 @@ Occurrences:
- Continues monthly...
```
#### Nth Day-of-Week of Month (Ordinal BYDAY) {#first-weekday-of-month}
**RRULE:** `FREQ=MONTHLY;BYDAY=1WE`
**Description:** Maintenance occurs on the first Wednesday of each month
**Use Case:** Monthly patch windows tied to a specific weekday
**Example:**
```
Title: Monthly Patch Window
Start: June 3, 2026 at 2:30 PM
RRULE: FREQ=MONTHLY;BYDAY=1WE
Duration: 90 minutes
Occurrences:
- Jun 3, 2026 at 2:30 PM
- Jul 1, 2026 at 2:30 PM
- Aug 5, 2026 at 2:30 PM
- Continues monthly...
```
#### Middle of Month {#middle-of-month}
**RRULE:** `FREQ=MONTHLY;BYMONTHDAY=15`
@@ -362,15 +386,16 @@ Affected Monitors:
Title: Monthly Security Updates
Description: Critical security patches and system updates
Start Date: Sunday, June 4, 2026 at 2:00 AM
RRULE: FREQ=MONTHLY;BYMONTHDAY=1
RRULE: FREQ=MONTHLY;BYDAY=1SU
Duration: 3600 seconds (1 hour)
Affected Monitors:
- All Servers: MAINTENANCE
```
**Note:** Since RRULE doesn't support "first Sunday", use BYMONTHDAY=1 and choose first occurrence that falls on Sunday, or use BYDAY with ordinal (not supported in Kener UI currently).
**Note:** Use ordinal weekday syntax in `BYDAY` for "first X weekday" patterns:
**Workaround:** Create as weekly and manage manually or use BYMONTHDAY for specific date.
- First Sunday: `BYDAY=1SU`
- First Wednesday: `BYDAY=1WE`
### Example 4: Business Hours Backup {#example-backup}
@@ -469,6 +494,7 @@ Start: 2:00 AM UTC
`FREQ=WEEKLY;BYDAY=MO,WE,FR`
`FREQ=WEEKLY;INTERVAL=2;BYDAY=SU`
`FREQ=MONTHLY;BYMONTHDAY=1`
`FREQ=MONTHLY;BYDAY=1WE`
`FREQ=MINUTELY;COUNT=1`
### Invalid RRULEs {#invalid-rrules}
@@ -602,6 +628,7 @@ Description: |
| Every Sunday | `FREQ=WEEKLY;BYDAY=SU` |
| Bi-weekly Mondays | `FREQ=WEEKLY;INTERVAL=2;BYDAY=MO` |
| First of month | `FREQ=MONTHLY;BYMONTHDAY=1` |
| First Wednesday | `FREQ=MONTHLY;BYDAY=1WE` |
| 15th of month | `FREQ=MONTHLY;BYMONTHDAY=15` |
| Quarterly | `FREQ=MONTHLY;INTERVAL=3;BYMONTHDAY=1` |
@@ -619,4 +646,4 @@ Description: |
- [Creating and Managing Maintenances](/docs/v4/maintenances/creating-managing) - Apply these patterns in the dashboard
- [Maintenance Events](/docs/v4/maintenances/events) - Understand how RRULE generates events
- [Maintenance Overview](/docs/v4/maintenances/overview) - Learn maintenance fundamentals
- [Maintenance Overview](/docs/v4/maintenances/overview) - Learn maintenance fundamentals
@@ -28,7 +28,7 @@
import { onMount } from "svelte";
import { goto } from "$app/navigation";
import { toast } from "svelte-sonner";
import { format, formatDistanceToNow, isPast, isFuture, isWithinInterval, addDays } from "date-fns";
import { format, formatDistanceToNow, isPast, isFuture, isWithinInterval } from "date-fns";
import { rrulestr } from "rrule";
import { resolve } from "$app/paths";
import clientResolver from "$lib/client/resolver.js";
@@ -193,9 +193,16 @@
const dtstart = new Date(startDateTimeLocal);
const fullRrule = `DTSTART:${dtstart.toISOString().replace(/[-:]/g, "").split(".")[0]}Z\nRRULE:${customRrule}`;
const rule = rrulestr(fullRrule);
const now = new Date();
const windowEnd = addDays(now, 30);
const occurrences = rule.between(now, windowEnd, true).slice(0, 5);
const occurrences: Date[] = [];
let searchFrom = new Date();
for (let i = 0; i < 5; i++) {
const next = rule.after(searchFrom, i === 0);
if (!next) break;
occurrences.push(next);
searchFrom = next;
}
return occurrences.map((d) => format(d, "EEE, MMM d, yyyy 'at' h:mm a"));
} catch (e) {
return [];
@@ -980,4 +987,4 @@
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
</Dialog.Root>