Compare commits

...

3 Commits

Author SHA1 Message Date
Jatinderjit Singh
0c5c21913e chore(planned-maintenance): remove unused function 2026-06-13 20:05:43 +05:30
Jatinderjit Singh
e31dd6169b docs(planned-maintenance): add description to endTime for forever downtime 2026-06-13 17:21:47 +05:30
Jatinderjit Singh
8b19804d0c feat(planned-maintenance): support indefinite ("forever") fixed downtimes
Make endTime optional for fixed schedules so a downtime can be set up to
be active until the user explicitly updates or deletes it. This matches
the existing behaviour for recurring schedules.
2026-06-13 16:59:18 +05:30
8 changed files with 44 additions and 41 deletions

View File

@@ -410,6 +410,7 @@ components:
duration:
type: string
repeatOn:
description: Required for weekly recurrence.
items:
$ref: '#/components/schemas/AlertmanagertypesRepeatOn'
nullable: true
@@ -439,6 +440,7 @@ components:
AlertmanagertypesSchedule:
properties:
endTime:
description: If empty, the alert will be active forever.
format: date-time
type: string
recurrence:

View File

@@ -415,6 +415,7 @@ export interface AlertmanagertypesRecurrenceDTO {
duration: string;
/**
* @type array,null
* @description Required for weekly recurrence.
*/
repeatOn?: AlertmanagertypesRepeatOnDTO[] | null;
repeatType: AlertmanagertypesRepeatTypeDTO;
@@ -424,6 +425,7 @@ export interface AlertmanagertypesScheduleDTO {
/**
* @type string
* @format date-time
* @description If empty, the alert will be active forever.
*/
endTime?: string;
recurrence?: AlertmanagertypesRecurrenceDTO;

View File

@@ -442,12 +442,6 @@ export function PlannedDowntimeForm(
<Form.Item
label="Ends on"
name="endTime"
required={recurrenceType === recurrenceOptions.doesNotRepeat.value}
rules={[
{
required: recurrenceType === recurrenceOptions.doesNotRepeat.value,
},
]}
className={!isEmpty(endTimeText) ? 'formItemWithBullet' : ''}
>
<DatePicker

View File

@@ -183,7 +183,9 @@ export function CollapseListContent({
{renderItems(
'Timeframe',
schedule?.startTime ? (
<Typography>{`${startTime}${endTime}`}</Typography>
<Typography>
{schedule?.endTime ? `${startTime}${endTime}` : `${startTime} onwards`}
</Typography>
) : (
'-'
),

View File

@@ -164,7 +164,7 @@ func (m *PlannedMaintenance) IsActive(now time.Time) bool {
return false
}
// Check if maintenance window has expired
// Check if the window has expired. Zero means it is active forever.
if !m.Schedule.EndTime.IsZero() && now.After(m.Schedule.EndTime) {
return false
}
@@ -187,6 +187,7 @@ func (m *PlannedMaintenance) IsActive(now time.Time) bool {
case RepeatTypeMonthly:
return m.checkMonthly(now, loc)
default:
// unreachable: invalid repeat type
return false
}
}
@@ -295,37 +296,6 @@ func (m *PlannedMaintenance) IsRecurring() bool {
return m.Schedule.Recurrence != nil
}
func (m *PlannedMaintenance) Validate() error {
if m.Name == "" {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing name in the payload")
}
if m.Schedule == nil {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing schedule in the payload")
}
if !m.Schedule.EndTime.IsZero() && m.Schedule.StartTime.After(m.Schedule.EndTime) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "start time cannot be after end time")
}
if m.Schedule.Recurrence != nil {
if m.Schedule.Recurrence.RepeatType.IsZero() {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing repeat type in the payload")
}
if m.Schedule.Recurrence.Duration.IsZero() {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing duration in the payload")
}
}
if m.Scope != "" {
if _, err := expr.Compile(m.Scope, expr.AllowUndefinedVariables(), expr.AsBool()); err != nil {
err := errors.Newf(
errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload,
"invalid scope: %s", err.Error(),
)
return err.WithUrl(scopeDocUrl)
}
}
return nil
}
func (m PlannedMaintenance) MarshalJSON() ([]byte, error) {
var status MaintenanceStatus
if m.IsActive(time.Now()) {

View File

@@ -436,6 +436,39 @@ func TestShouldSkipMaintenance(t *testing.T) {
ts: time.Now().UTC().Add(-time.Hour),
skip: false,
},
{
name: "fixed planned maintenance with no end time (forever), ts > start",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Now().UTC().Add(-time.Hour),
},
},
ts: time.Now().UTC(),
skip: true,
},
{
name: "fixed planned maintenance with no end time (forever), ts == start",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
},
},
ts: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
skip: true,
},
{
name: "fixed planned maintenance with no end time (forever), ts < start",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Now().UTC().Add(time.Hour),
},
},
ts: time.Now().UTC(),
skip: false,
},
{
name: "recurring maintenance, repeat sunday, saturday, weekly for 24 hours, in Us/Eastern timezone",
maintenance: &PlannedMaintenance{

View File

@@ -71,7 +71,7 @@ var RepeatOnAllMap = map[RepeatOn]time.Weekday{
type Recurrence struct {
Duration valuer.TextDuration `json:"duration" required:"true"`
RepeatType RepeatType `json:"repeatType" required:"true"`
RepeatOn []RepeatOn `json:"repeatOn"`
RepeatOn []RepeatOn `json:"repeatOn" description:"Required for weekly recurrence."`
}
func (r *Recurrence) Scan(src interface{}) error {

View File

@@ -12,7 +12,7 @@ import (
type Schedule struct {
Timezone string `json:"timezone" required:"true"`
StartTime time.Time `json:"startTime" required:"true"`
EndTime time.Time `json:"endTime,omitzero"`
EndTime time.Time `json:"endTime,omitzero" description:"If empty, the alert will be active forever."`
Recurrence *Recurrence `json:"recurrence"`
}