@checkstack/maintenance-backend 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/drizzle/0001_tough_star_brand.sql +1 -0
- package/drizzle/meta/0001_snapshot.json +214 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/src/router.ts +20 -13
- package/src/schema.ts +5 -1
- package/src/service.ts +48 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @checkstack/maintenance-backend
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 18fa8e3: Add notification suppression toggle for maintenance windows
|
|
8
|
+
|
|
9
|
+
**New Feature:** When creating or editing a maintenance window, you can now enable "Suppress health notifications" to prevent health status change notifications from being sent for affected systems while the maintenance is active (in_progress status). This is useful for planned downtime where health alerts are expected and would otherwise create noise.
|
|
10
|
+
|
|
11
|
+
**Changes:**
|
|
12
|
+
|
|
13
|
+
- Added `suppressNotifications` field to maintenance schema
|
|
14
|
+
- Added new service-to-service API `hasActiveMaintenanceWithSuppression`
|
|
15
|
+
- Healthcheck queue executor now checks for suppression before sending notifications
|
|
16
|
+
- MaintenanceEditor UI includes new toggle checkbox
|
|
17
|
+
|
|
18
|
+
**Bug Fix:** Fixed migration system to correctly set PostgreSQL search_path when running plugin migrations. Previously, migrations could fail with "relation does not exist" errors because the schema context wasn't properly set.
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- Updated dependencies [18fa8e3]
|
|
23
|
+
- @checkstack/maintenance-common@0.4.0
|
|
24
|
+
|
|
3
25
|
## 0.2.5
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE "maintenances" ADD COLUMN "suppress_notifications" boolean DEFAULT false NOT NULL;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "d0a42a3a-4685-408b-8ad9-e2c0ac19e8a2",
|
|
3
|
+
"prevId": "2722e752-6079-4992-b4d1-da0a4bc0e01b",
|
|
4
|
+
"version": "7",
|
|
5
|
+
"dialect": "postgresql",
|
|
6
|
+
"tables": {
|
|
7
|
+
"public.maintenance_systems": {
|
|
8
|
+
"name": "maintenance_systems",
|
|
9
|
+
"schema": "",
|
|
10
|
+
"columns": {
|
|
11
|
+
"maintenance_id": {
|
|
12
|
+
"name": "maintenance_id",
|
|
13
|
+
"type": "text",
|
|
14
|
+
"primaryKey": false,
|
|
15
|
+
"notNull": true
|
|
16
|
+
},
|
|
17
|
+
"system_id": {
|
|
18
|
+
"name": "system_id",
|
|
19
|
+
"type": "text",
|
|
20
|
+
"primaryKey": false,
|
|
21
|
+
"notNull": true
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"indexes": {},
|
|
25
|
+
"foreignKeys": {
|
|
26
|
+
"maintenance_systems_maintenance_id_maintenances_id_fk": {
|
|
27
|
+
"name": "maintenance_systems_maintenance_id_maintenances_id_fk",
|
|
28
|
+
"tableFrom": "maintenance_systems",
|
|
29
|
+
"tableTo": "maintenances",
|
|
30
|
+
"columnsFrom": [
|
|
31
|
+
"maintenance_id"
|
|
32
|
+
],
|
|
33
|
+
"columnsTo": [
|
|
34
|
+
"id"
|
|
35
|
+
],
|
|
36
|
+
"onDelete": "cascade",
|
|
37
|
+
"onUpdate": "no action"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"compositePrimaryKeys": {
|
|
41
|
+
"maintenance_systems_maintenance_id_system_id_pk": {
|
|
42
|
+
"name": "maintenance_systems_maintenance_id_system_id_pk",
|
|
43
|
+
"columns": [
|
|
44
|
+
"maintenance_id",
|
|
45
|
+
"system_id"
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"uniqueConstraints": {},
|
|
50
|
+
"policies": {},
|
|
51
|
+
"checkConstraints": {},
|
|
52
|
+
"isRLSEnabled": false
|
|
53
|
+
},
|
|
54
|
+
"public.maintenance_updates": {
|
|
55
|
+
"name": "maintenance_updates",
|
|
56
|
+
"schema": "",
|
|
57
|
+
"columns": {
|
|
58
|
+
"id": {
|
|
59
|
+
"name": "id",
|
|
60
|
+
"type": "text",
|
|
61
|
+
"primaryKey": true,
|
|
62
|
+
"notNull": true
|
|
63
|
+
},
|
|
64
|
+
"maintenance_id": {
|
|
65
|
+
"name": "maintenance_id",
|
|
66
|
+
"type": "text",
|
|
67
|
+
"primaryKey": false,
|
|
68
|
+
"notNull": true
|
|
69
|
+
},
|
|
70
|
+
"message": {
|
|
71
|
+
"name": "message",
|
|
72
|
+
"type": "text",
|
|
73
|
+
"primaryKey": false,
|
|
74
|
+
"notNull": true
|
|
75
|
+
},
|
|
76
|
+
"status_change": {
|
|
77
|
+
"name": "status_change",
|
|
78
|
+
"type": "maintenance_status",
|
|
79
|
+
"typeSchema": "public",
|
|
80
|
+
"primaryKey": false,
|
|
81
|
+
"notNull": false
|
|
82
|
+
},
|
|
83
|
+
"created_at": {
|
|
84
|
+
"name": "created_at",
|
|
85
|
+
"type": "timestamp",
|
|
86
|
+
"primaryKey": false,
|
|
87
|
+
"notNull": true,
|
|
88
|
+
"default": "now()"
|
|
89
|
+
},
|
|
90
|
+
"created_by": {
|
|
91
|
+
"name": "created_by",
|
|
92
|
+
"type": "text",
|
|
93
|
+
"primaryKey": false,
|
|
94
|
+
"notNull": false
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"indexes": {},
|
|
98
|
+
"foreignKeys": {
|
|
99
|
+
"maintenance_updates_maintenance_id_maintenances_id_fk": {
|
|
100
|
+
"name": "maintenance_updates_maintenance_id_maintenances_id_fk",
|
|
101
|
+
"tableFrom": "maintenance_updates",
|
|
102
|
+
"tableTo": "maintenances",
|
|
103
|
+
"columnsFrom": [
|
|
104
|
+
"maintenance_id"
|
|
105
|
+
],
|
|
106
|
+
"columnsTo": [
|
|
107
|
+
"id"
|
|
108
|
+
],
|
|
109
|
+
"onDelete": "cascade",
|
|
110
|
+
"onUpdate": "no action"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"compositePrimaryKeys": {},
|
|
114
|
+
"uniqueConstraints": {},
|
|
115
|
+
"policies": {},
|
|
116
|
+
"checkConstraints": {},
|
|
117
|
+
"isRLSEnabled": false
|
|
118
|
+
},
|
|
119
|
+
"public.maintenances": {
|
|
120
|
+
"name": "maintenances",
|
|
121
|
+
"schema": "",
|
|
122
|
+
"columns": {
|
|
123
|
+
"id": {
|
|
124
|
+
"name": "id",
|
|
125
|
+
"type": "text",
|
|
126
|
+
"primaryKey": true,
|
|
127
|
+
"notNull": true
|
|
128
|
+
},
|
|
129
|
+
"title": {
|
|
130
|
+
"name": "title",
|
|
131
|
+
"type": "text",
|
|
132
|
+
"primaryKey": false,
|
|
133
|
+
"notNull": true
|
|
134
|
+
},
|
|
135
|
+
"description": {
|
|
136
|
+
"name": "description",
|
|
137
|
+
"type": "text",
|
|
138
|
+
"primaryKey": false,
|
|
139
|
+
"notNull": false
|
|
140
|
+
},
|
|
141
|
+
"suppress_notifications": {
|
|
142
|
+
"name": "suppress_notifications",
|
|
143
|
+
"type": "boolean",
|
|
144
|
+
"primaryKey": false,
|
|
145
|
+
"notNull": true,
|
|
146
|
+
"default": false
|
|
147
|
+
},
|
|
148
|
+
"status": {
|
|
149
|
+
"name": "status",
|
|
150
|
+
"type": "maintenance_status",
|
|
151
|
+
"typeSchema": "public",
|
|
152
|
+
"primaryKey": false,
|
|
153
|
+
"notNull": true,
|
|
154
|
+
"default": "'scheduled'"
|
|
155
|
+
},
|
|
156
|
+
"start_at": {
|
|
157
|
+
"name": "start_at",
|
|
158
|
+
"type": "timestamp",
|
|
159
|
+
"primaryKey": false,
|
|
160
|
+
"notNull": true
|
|
161
|
+
},
|
|
162
|
+
"end_at": {
|
|
163
|
+
"name": "end_at",
|
|
164
|
+
"type": "timestamp",
|
|
165
|
+
"primaryKey": false,
|
|
166
|
+
"notNull": true
|
|
167
|
+
},
|
|
168
|
+
"created_at": {
|
|
169
|
+
"name": "created_at",
|
|
170
|
+
"type": "timestamp",
|
|
171
|
+
"primaryKey": false,
|
|
172
|
+
"notNull": true,
|
|
173
|
+
"default": "now()"
|
|
174
|
+
},
|
|
175
|
+
"updated_at": {
|
|
176
|
+
"name": "updated_at",
|
|
177
|
+
"type": "timestamp",
|
|
178
|
+
"primaryKey": false,
|
|
179
|
+
"notNull": true,
|
|
180
|
+
"default": "now()"
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
"indexes": {},
|
|
184
|
+
"foreignKeys": {},
|
|
185
|
+
"compositePrimaryKeys": {},
|
|
186
|
+
"uniqueConstraints": {},
|
|
187
|
+
"policies": {},
|
|
188
|
+
"checkConstraints": {},
|
|
189
|
+
"isRLSEnabled": false
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
"enums": {
|
|
193
|
+
"public.maintenance_status": {
|
|
194
|
+
"name": "maintenance_status",
|
|
195
|
+
"schema": "public",
|
|
196
|
+
"values": [
|
|
197
|
+
"scheduled",
|
|
198
|
+
"in_progress",
|
|
199
|
+
"completed",
|
|
200
|
+
"cancelled"
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
"schemas": {},
|
|
205
|
+
"sequences": {},
|
|
206
|
+
"roles": {},
|
|
207
|
+
"policies": {},
|
|
208
|
+
"views": {},
|
|
209
|
+
"_meta": {
|
|
210
|
+
"columns": {},
|
|
211
|
+
"schemas": {},
|
|
212
|
+
"tables": {}
|
|
213
|
+
}
|
|
214
|
+
}
|
package/package.json
CHANGED
package/src/router.ts
CHANGED
|
@@ -20,7 +20,7 @@ export function createRouter(
|
|
|
20
20
|
service: MaintenanceService,
|
|
21
21
|
signalService: SignalService,
|
|
22
22
|
catalogClient: InferClient<typeof CatalogApi>,
|
|
23
|
-
logger: Logger
|
|
23
|
+
logger: Logger,
|
|
24
24
|
) {
|
|
25
25
|
const os = implement(maintenanceContract)
|
|
26
26
|
.$context<RpcContext>()
|
|
@@ -44,7 +44,7 @@ export function createRouter(
|
|
|
44
44
|
maintenanceRoutes.routes.detail,
|
|
45
45
|
{
|
|
46
46
|
maintenanceId,
|
|
47
|
-
}
|
|
47
|
+
},
|
|
48
48
|
);
|
|
49
49
|
|
|
50
50
|
for (const systemId of systemIds) {
|
|
@@ -61,7 +61,7 @@ export function createRouter(
|
|
|
61
61
|
// Log but don't fail the operation - notifications are best-effort
|
|
62
62
|
logger.warn(
|
|
63
63
|
`Failed to notify subscribers for system ${systemId}:`,
|
|
64
|
-
error
|
|
64
|
+
error,
|
|
65
65
|
);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -81,7 +81,7 @@ export function createRouter(
|
|
|
81
81
|
getMaintenancesForSystem: os.getMaintenancesForSystem.handler(
|
|
82
82
|
async ({ input }) => {
|
|
83
83
|
return service.getMaintenancesForSystem(input.systemId);
|
|
84
|
-
}
|
|
84
|
+
},
|
|
85
85
|
),
|
|
86
86
|
|
|
87
87
|
getBulkMaintenancesForSystems: os.getBulkMaintenancesForSystems.handler(
|
|
@@ -94,14 +94,13 @@ export function createRouter(
|
|
|
94
94
|
// Fetch maintenances for each system in parallel
|
|
95
95
|
await Promise.all(
|
|
96
96
|
input.systemIds.map(async (systemId) => {
|
|
97
|
-
maintenances[systemId] =
|
|
98
|
-
systemId
|
|
99
|
-
|
|
100
|
-
})
|
|
97
|
+
maintenances[systemId] =
|
|
98
|
+
await service.getMaintenancesForSystem(systemId);
|
|
99
|
+
}),
|
|
101
100
|
);
|
|
102
101
|
|
|
103
102
|
return { maintenances };
|
|
104
|
-
}
|
|
103
|
+
},
|
|
105
104
|
),
|
|
106
105
|
|
|
107
106
|
createMaintenance: os.createMaintenance.handler(
|
|
@@ -135,7 +134,7 @@ export function createRouter(
|
|
|
135
134
|
});
|
|
136
135
|
|
|
137
136
|
return result;
|
|
138
|
-
}
|
|
137
|
+
},
|
|
139
138
|
),
|
|
140
139
|
|
|
141
140
|
updateMaintenance: os.updateMaintenance.handler(
|
|
@@ -175,7 +174,7 @@ export function createRouter(
|
|
|
175
174
|
});
|
|
176
175
|
|
|
177
176
|
return result;
|
|
178
|
-
}
|
|
177
|
+
},
|
|
179
178
|
),
|
|
180
179
|
|
|
181
180
|
addUpdate: os.addUpdate.handler(async ({ input, context }) => {
|
|
@@ -213,7 +212,7 @@ export function createRouter(
|
|
|
213
212
|
const result = await service.closeMaintenance(
|
|
214
213
|
input.id,
|
|
215
214
|
input.message,
|
|
216
|
-
userId
|
|
215
|
+
userId,
|
|
217
216
|
);
|
|
218
217
|
if (!result) {
|
|
219
218
|
throw new ORPCError("NOT_FOUND", {
|
|
@@ -240,7 +239,7 @@ export function createRouter(
|
|
|
240
239
|
});
|
|
241
240
|
|
|
242
241
|
return result;
|
|
243
|
-
}
|
|
242
|
+
},
|
|
244
243
|
),
|
|
245
244
|
|
|
246
245
|
deleteMaintenance: os.deleteMaintenance.handler(async ({ input }) => {
|
|
@@ -256,5 +255,13 @@ export function createRouter(
|
|
|
256
255
|
}
|
|
257
256
|
return { success };
|
|
258
257
|
}),
|
|
258
|
+
|
|
259
|
+
hasActiveMaintenanceWithSuppression:
|
|
260
|
+
os.hasActiveMaintenanceWithSuppression.handler(async ({ input }) => {
|
|
261
|
+
const suppressed = await service.hasActiveMaintenanceWithSuppression(
|
|
262
|
+
input.systemId,
|
|
263
|
+
);
|
|
264
|
+
return { suppressed };
|
|
265
|
+
}),
|
|
259
266
|
});
|
|
260
267
|
}
|
package/src/schema.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
text,
|
|
5
5
|
timestamp,
|
|
6
6
|
primaryKey,
|
|
7
|
+
boolean,
|
|
7
8
|
} from "drizzle-orm/pg-core";
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -23,6 +24,9 @@ export const maintenances = pgTable("maintenances", {
|
|
|
23
24
|
id: text("id").primaryKey(),
|
|
24
25
|
title: text("title").notNull(),
|
|
25
26
|
description: text("description"),
|
|
27
|
+
suppressNotifications: boolean("suppress_notifications")
|
|
28
|
+
.notNull()
|
|
29
|
+
.default(false),
|
|
26
30
|
status: maintenanceStatusEnum("status").notNull().default("scheduled"),
|
|
27
31
|
startAt: timestamp("start_at").notNull(),
|
|
28
32
|
endAt: timestamp("end_at").notNull(),
|
|
@@ -43,7 +47,7 @@ export const maintenanceSystems = pgTable(
|
|
|
43
47
|
},
|
|
44
48
|
(t) => ({
|
|
45
49
|
pk: primaryKey(t.maintenanceId, t.systemId),
|
|
46
|
-
})
|
|
50
|
+
}),
|
|
47
51
|
);
|
|
48
52
|
|
|
49
53
|
/**
|
package/src/service.ts
CHANGED
|
@@ -46,15 +46,17 @@ export class MaintenanceService {
|
|
|
46
46
|
.where(
|
|
47
47
|
and(
|
|
48
48
|
inArray(maintenances.id, ids),
|
|
49
|
-
filters.status
|
|
50
|
-
|
|
49
|
+
filters.status
|
|
50
|
+
? eq(maintenances.status, filters.status)
|
|
51
|
+
: undefined,
|
|
52
|
+
),
|
|
51
53
|
);
|
|
52
54
|
} else {
|
|
53
55
|
maintenanceRows = await this.db
|
|
54
56
|
.select()
|
|
55
57
|
.from(maintenances)
|
|
56
58
|
.where(
|
|
57
|
-
filters?.status ? eq(maintenances.status, filters.status) : undefined
|
|
59
|
+
filters?.status ? eq(maintenances.status, filters.status) : undefined,
|
|
58
60
|
);
|
|
59
61
|
}
|
|
60
62
|
|
|
@@ -113,7 +115,7 @@ export class MaintenanceService {
|
|
|
113
115
|
* Get active/upcoming maintenances for a system
|
|
114
116
|
*/
|
|
115
117
|
async getMaintenancesForSystem(
|
|
116
|
-
systemId: string
|
|
118
|
+
systemId: string,
|
|
117
119
|
): Promise<MaintenanceWithSystems[]> {
|
|
118
120
|
const _now = new Date();
|
|
119
121
|
|
|
@@ -135,9 +137,9 @@ export class MaintenanceService {
|
|
|
135
137
|
inArray(maintenances.id, ids),
|
|
136
138
|
or(
|
|
137
139
|
eq(maintenances.status, "scheduled"),
|
|
138
|
-
eq(maintenances.status, "in_progress")
|
|
139
|
-
)
|
|
140
|
-
)
|
|
140
|
+
eq(maintenances.status, "in_progress"),
|
|
141
|
+
),
|
|
142
|
+
),
|
|
141
143
|
);
|
|
142
144
|
|
|
143
145
|
// Fetch system IDs for each
|
|
@@ -162,7 +164,7 @@ export class MaintenanceService {
|
|
|
162
164
|
* Create a new maintenance
|
|
163
165
|
*/
|
|
164
166
|
async createMaintenance(
|
|
165
|
-
input: CreateMaintenanceInput
|
|
167
|
+
input: CreateMaintenanceInput,
|
|
166
168
|
): Promise<MaintenanceWithSystems> {
|
|
167
169
|
const id = generateId();
|
|
168
170
|
|
|
@@ -170,6 +172,7 @@ export class MaintenanceService {
|
|
|
170
172
|
id,
|
|
171
173
|
title: input.title,
|
|
172
174
|
description: input.description,
|
|
175
|
+
suppressNotifications: input.suppressNotifications ?? false,
|
|
173
176
|
status: "scheduled",
|
|
174
177
|
startAt: input.startAt,
|
|
175
178
|
endAt: input.endAt,
|
|
@@ -190,7 +193,7 @@ export class MaintenanceService {
|
|
|
190
193
|
* Update an existing maintenance
|
|
191
194
|
*/
|
|
192
195
|
async updateMaintenance(
|
|
193
|
-
input: UpdateMaintenanceInput
|
|
196
|
+
input: UpdateMaintenanceInput,
|
|
194
197
|
): Promise<MaintenanceWithSystems | undefined> {
|
|
195
198
|
const [existing] = await this.db
|
|
196
199
|
.select()
|
|
@@ -206,6 +209,8 @@ export class MaintenanceService {
|
|
|
206
209
|
if (input.title !== undefined) updateData.title = input.title;
|
|
207
210
|
if (input.description !== undefined)
|
|
208
211
|
updateData.description = input.description;
|
|
212
|
+
if (input.suppressNotifications !== undefined)
|
|
213
|
+
updateData.suppressNotifications = input.suppressNotifications;
|
|
209
214
|
if (input.startAt !== undefined) updateData.startAt = input.startAt;
|
|
210
215
|
if (input.endAt !== undefined) updateData.endAt = input.endAt;
|
|
211
216
|
|
|
@@ -236,7 +241,7 @@ export class MaintenanceService {
|
|
|
236
241
|
*/
|
|
237
242
|
async addUpdate(
|
|
238
243
|
input: AddMaintenanceUpdateInput,
|
|
239
|
-
userId?: string
|
|
244
|
+
userId?: string,
|
|
240
245
|
): Promise<MaintenanceUpdate> {
|
|
241
246
|
const id = generateId();
|
|
242
247
|
|
|
@@ -274,7 +279,7 @@ export class MaintenanceService {
|
|
|
274
279
|
async closeMaintenance(
|
|
275
280
|
id: string,
|
|
276
281
|
message?: string,
|
|
277
|
-
userId?: string
|
|
282
|
+
userId?: string,
|
|
278
283
|
): Promise<MaintenanceWithSystems | undefined> {
|
|
279
284
|
const [existing] = await this.db
|
|
280
285
|
.select()
|
|
@@ -325,4 +330,36 @@ export class MaintenanceService {
|
|
|
325
330
|
// For now, return empty set
|
|
326
331
|
return new Set();
|
|
327
332
|
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Check if a system has an active maintenance with notification suppression enabled.
|
|
336
|
+
* A maintenance is considered "active" if its status is "in_progress".
|
|
337
|
+
*/
|
|
338
|
+
async hasActiveMaintenanceWithSuppression(
|
|
339
|
+
systemId: string,
|
|
340
|
+
): Promise<boolean> {
|
|
341
|
+
// Get maintenance IDs for this system
|
|
342
|
+
const systemMaintenances = await this.db
|
|
343
|
+
.select({ maintenanceId: maintenanceSystems.maintenanceId })
|
|
344
|
+
.from(maintenanceSystems)
|
|
345
|
+
.where(eq(maintenanceSystems.systemId, systemId));
|
|
346
|
+
|
|
347
|
+
const ids = systemMaintenances.map((r) => r.maintenanceId);
|
|
348
|
+
if (ids.length === 0) return false;
|
|
349
|
+
|
|
350
|
+
// Check if any of these maintenances are in_progress with suppressNotifications enabled
|
|
351
|
+
const [match] = await this.db
|
|
352
|
+
.select({ id: maintenances.id })
|
|
353
|
+
.from(maintenances)
|
|
354
|
+
.where(
|
|
355
|
+
and(
|
|
356
|
+
inArray(maintenances.id, ids),
|
|
357
|
+
eq(maintenances.status, "in_progress"),
|
|
358
|
+
eq(maintenances.suppressNotifications, true),
|
|
359
|
+
),
|
|
360
|
+
)
|
|
361
|
+
.limit(1);
|
|
362
|
+
|
|
363
|
+
return !!match;
|
|
364
|
+
}
|
|
328
365
|
}
|