@oneuptime/common 9.2.17 → 9.2.18
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/Server/API/AlertAPI.ts +139 -0
- package/Server/API/IncidentAPI.ts +132 -0
- package/Server/API/ScheduledMaintenanceAPI.ts +164 -0
- package/Server/Services/AIService.ts +0 -1
- package/Server/Services/IncidentService.ts +0 -1
- package/Server/Utils/AI/AlertAIContextBuilder.ts +264 -0
- package/Server/Utils/AI/IncidentAIContextBuilder.ts +212 -0
- package/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.ts +345 -0
- package/Tests/Types/Domain.test.ts +24 -3
- package/Types/Domain.ts +21 -24
- package/UI/Components/AI/GenerateFromAIModal.tsx +157 -20
- package/build/dist/Server/API/AlertAPI.js +94 -0
- package/build/dist/Server/API/AlertAPI.js.map +1 -0
- package/build/dist/Server/API/IncidentAPI.js +88 -1
- package/build/dist/Server/API/IncidentAPI.js.map +1 -1
- package/build/dist/Server/API/ScheduledMaintenanceAPI.js +103 -0
- package/build/dist/Server/API/ScheduledMaintenanceAPI.js.map +1 -0
- package/build/dist/Server/Services/AIService.js +0 -1
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +0 -1
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js +238 -0
- package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js.map +1 -0
- package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js +189 -0
- package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js.map +1 -1
- package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js +311 -0
- package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js.map +1 -0
- package/build/dist/Tests/Types/Domain.test.js +19 -3
- package/build/dist/Tests/Types/Domain.test.js.map +1 -1
- package/build/dist/Types/Domain.js +18 -16
- package/build/dist/Types/Domain.js.map +1 -1
- package/build/dist/UI/Components/AI/GenerateFromAIModal.js +116 -3
- package/build/dist/UI/Components/AI/GenerateFromAIModal.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
2
|
+
import ScheduledMaintenance from "../../../Models/DatabaseModels/ScheduledMaintenance";
|
|
3
|
+
import ScheduledMaintenanceStateTimeline from "../../../Models/DatabaseModels/ScheduledMaintenanceStateTimeline";
|
|
4
|
+
import ScheduledMaintenanceInternalNote from "../../../Models/DatabaseModels/ScheduledMaintenanceInternalNote";
|
|
5
|
+
import ScheduledMaintenancePublicNote from "../../../Models/DatabaseModels/ScheduledMaintenancePublicNote";
|
|
6
|
+
import ScheduledMaintenanceService from "../../Services/ScheduledMaintenanceService";
|
|
7
|
+
import ScheduledMaintenanceStateTimelineService from "../../Services/ScheduledMaintenanceStateTimelineService";
|
|
8
|
+
import ScheduledMaintenanceInternalNoteService from "../../Services/ScheduledMaintenanceInternalNoteService";
|
|
9
|
+
import ScheduledMaintenancePublicNoteService from "../../Services/ScheduledMaintenancePublicNoteService";
|
|
10
|
+
import CaptureSpan from "../Telemetry/CaptureSpan";
|
|
11
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
12
|
+
import SortOrder from "../../../Types/BaseDatabase/SortOrder";
|
|
13
|
+
import { LLMMessage } from "../LLM/LLMService";
|
|
14
|
+
|
|
15
|
+
export interface ScheduledMaintenanceContextData {
|
|
16
|
+
scheduledMaintenance: ScheduledMaintenance;
|
|
17
|
+
stateTimeline: Array<ScheduledMaintenanceStateTimeline>;
|
|
18
|
+
internalNotes: Array<ScheduledMaintenanceInternalNote>;
|
|
19
|
+
publicNotes: Array<ScheduledMaintenancePublicNote>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AIGenerationContext {
|
|
23
|
+
contextText: string;
|
|
24
|
+
systemPrompt: string;
|
|
25
|
+
messages: Array<LLMMessage>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default class ScheduledMaintenanceAIContextBuilder {
|
|
29
|
+
@CaptureSpan()
|
|
30
|
+
public static async buildScheduledMaintenanceContext(data: {
|
|
31
|
+
scheduledMaintenanceId: ObjectID;
|
|
32
|
+
}): Promise<ScheduledMaintenanceContextData> {
|
|
33
|
+
const scheduledMaintenance: ScheduledMaintenance | null =
|
|
34
|
+
await ScheduledMaintenanceService.findOneById({
|
|
35
|
+
id: data.scheduledMaintenanceId,
|
|
36
|
+
select: {
|
|
37
|
+
_id: true,
|
|
38
|
+
title: true,
|
|
39
|
+
description: true,
|
|
40
|
+
createdAt: true,
|
|
41
|
+
startsAt: true,
|
|
42
|
+
endsAt: true,
|
|
43
|
+
customFields: true,
|
|
44
|
+
projectId: true,
|
|
45
|
+
currentScheduledMaintenanceState: {
|
|
46
|
+
name: true,
|
|
47
|
+
color: true,
|
|
48
|
+
},
|
|
49
|
+
monitors: {
|
|
50
|
+
name: true,
|
|
51
|
+
},
|
|
52
|
+
labels: {
|
|
53
|
+
name: true,
|
|
54
|
+
color: true,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
props: {
|
|
58
|
+
isRoot: true,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!scheduledMaintenance) {
|
|
63
|
+
throw new Error("Scheduled Maintenance not found");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Fetch state timeline
|
|
67
|
+
const stateTimeline: Array<ScheduledMaintenanceStateTimeline> =
|
|
68
|
+
await ScheduledMaintenanceStateTimelineService.findBy({
|
|
69
|
+
query: {
|
|
70
|
+
scheduledMaintenanceId: data.scheduledMaintenanceId,
|
|
71
|
+
},
|
|
72
|
+
select: {
|
|
73
|
+
_id: true,
|
|
74
|
+
createdAt: true,
|
|
75
|
+
startsAt: true,
|
|
76
|
+
endsAt: true,
|
|
77
|
+
scheduledMaintenanceState: {
|
|
78
|
+
name: true,
|
|
79
|
+
color: true,
|
|
80
|
+
},
|
|
81
|
+
createdByUser: {
|
|
82
|
+
name: true,
|
|
83
|
+
email: true,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
sort: {
|
|
87
|
+
startsAt: SortOrder.Ascending,
|
|
88
|
+
},
|
|
89
|
+
limit: 100,
|
|
90
|
+
skip: 0,
|
|
91
|
+
props: {
|
|
92
|
+
isRoot: true,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Fetch internal notes
|
|
97
|
+
const internalNotes: Array<ScheduledMaintenanceInternalNote> =
|
|
98
|
+
await ScheduledMaintenanceInternalNoteService.findBy({
|
|
99
|
+
query: {
|
|
100
|
+
scheduledMaintenanceId: data.scheduledMaintenanceId,
|
|
101
|
+
},
|
|
102
|
+
select: {
|
|
103
|
+
_id: true,
|
|
104
|
+
note: true,
|
|
105
|
+
createdAt: true,
|
|
106
|
+
createdByUser: {
|
|
107
|
+
name: true,
|
|
108
|
+
email: true,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
sort: {
|
|
112
|
+
createdAt: SortOrder.Ascending,
|
|
113
|
+
},
|
|
114
|
+
limit: 100,
|
|
115
|
+
skip: 0,
|
|
116
|
+
props: {
|
|
117
|
+
isRoot: true,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Fetch public notes
|
|
122
|
+
const publicNotes: Array<ScheduledMaintenancePublicNote> =
|
|
123
|
+
await ScheduledMaintenancePublicNoteService.findBy({
|
|
124
|
+
query: {
|
|
125
|
+
scheduledMaintenanceId: data.scheduledMaintenanceId,
|
|
126
|
+
},
|
|
127
|
+
select: {
|
|
128
|
+
_id: true,
|
|
129
|
+
note: true,
|
|
130
|
+
createdAt: true,
|
|
131
|
+
postedAt: true,
|
|
132
|
+
createdByUser: {
|
|
133
|
+
name: true,
|
|
134
|
+
email: true,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
sort: {
|
|
138
|
+
createdAt: SortOrder.Ascending,
|
|
139
|
+
},
|
|
140
|
+
limit: 100,
|
|
141
|
+
skip: 0,
|
|
142
|
+
props: {
|
|
143
|
+
isRoot: true,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
scheduledMaintenance,
|
|
149
|
+
stateTimeline,
|
|
150
|
+
internalNotes,
|
|
151
|
+
publicNotes,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@CaptureSpan()
|
|
156
|
+
public static formatScheduledMaintenanceContextForNote(
|
|
157
|
+
contextData: ScheduledMaintenanceContextData,
|
|
158
|
+
noteType: "public" | "internal",
|
|
159
|
+
template?: string,
|
|
160
|
+
): AIGenerationContext {
|
|
161
|
+
const { scheduledMaintenance, stateTimeline, internalNotes, publicNotes } =
|
|
162
|
+
contextData;
|
|
163
|
+
|
|
164
|
+
let contextText: string = "";
|
|
165
|
+
|
|
166
|
+
// Basic scheduled maintenance information
|
|
167
|
+
contextText += "# Scheduled Maintenance Information\n\n";
|
|
168
|
+
contextText += `**Title:** ${scheduledMaintenance.title || "N/A"}\n\n`;
|
|
169
|
+
contextText += `**Description:** ${scheduledMaintenance.description || "N/A"}\n\n`;
|
|
170
|
+
contextText += `**Current State:** ${scheduledMaintenance.currentScheduledMaintenanceState?.name || "N/A"}\n\n`;
|
|
171
|
+
contextText += `**Scheduled Start:** ${scheduledMaintenance.startsAt ? OneUptimeDate.getDateAsFormattedString(scheduledMaintenance.startsAt) : "N/A"}\n\n`;
|
|
172
|
+
contextText += `**Scheduled End:** ${scheduledMaintenance.endsAt ? OneUptimeDate.getDateAsFormattedString(scheduledMaintenance.endsAt) : "N/A"}\n\n`;
|
|
173
|
+
contextText += `**Created At:** ${scheduledMaintenance.createdAt ? OneUptimeDate.getDateAsFormattedString(scheduledMaintenance.createdAt) : "N/A"}\n\n`;
|
|
174
|
+
|
|
175
|
+
// Affected monitors
|
|
176
|
+
if (
|
|
177
|
+
scheduledMaintenance.monitors &&
|
|
178
|
+
scheduledMaintenance.monitors.length > 0
|
|
179
|
+
) {
|
|
180
|
+
contextText += "**Affected Monitors:** ";
|
|
181
|
+
contextText += scheduledMaintenance.monitors
|
|
182
|
+
.map((m: { name?: string }) => {
|
|
183
|
+
return m.name;
|
|
184
|
+
})
|
|
185
|
+
.join(", ");
|
|
186
|
+
contextText += "\n\n";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Labels
|
|
190
|
+
if (scheduledMaintenance.labels && scheduledMaintenance.labels.length > 0) {
|
|
191
|
+
contextText += "**Labels:** ";
|
|
192
|
+
contextText += scheduledMaintenance.labels
|
|
193
|
+
.map((l: { name?: string }) => {
|
|
194
|
+
return l.name;
|
|
195
|
+
})
|
|
196
|
+
.join(", ");
|
|
197
|
+
contextText += "\n\n";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// State timeline
|
|
201
|
+
if (stateTimeline.length > 0) {
|
|
202
|
+
contextText += "# State Timeline\n\n";
|
|
203
|
+
for (const timeline of stateTimeline) {
|
|
204
|
+
const startTime: string = timeline.startsAt
|
|
205
|
+
? OneUptimeDate.getDateAsFormattedString(timeline.startsAt)
|
|
206
|
+
: "N/A";
|
|
207
|
+
const stateName: string =
|
|
208
|
+
timeline.scheduledMaintenanceState?.name?.toString() || "Unknown";
|
|
209
|
+
const createdBy: string =
|
|
210
|
+
timeline.createdByUser?.name?.toString() ||
|
|
211
|
+
timeline.createdByUser?.email?.toString() ||
|
|
212
|
+
"System";
|
|
213
|
+
|
|
214
|
+
contextText += `- **${startTime}**: State changed to **${stateName}** by ${createdBy}\n`;
|
|
215
|
+
}
|
|
216
|
+
contextText += "\n";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Include internal notes for context (for both note types)
|
|
220
|
+
if (internalNotes.length > 0) {
|
|
221
|
+
contextText += "# Internal Notes (Private)\n\n";
|
|
222
|
+
for (const note of internalNotes) {
|
|
223
|
+
const noteTime: string = note.createdAt
|
|
224
|
+
? OneUptimeDate.getDateAsFormattedString(note.createdAt)
|
|
225
|
+
: "N/A";
|
|
226
|
+
const createdBy: string =
|
|
227
|
+
note.createdByUser?.name?.toString() ||
|
|
228
|
+
note.createdByUser?.email?.toString() ||
|
|
229
|
+
"Unknown";
|
|
230
|
+
|
|
231
|
+
contextText += `**[${noteTime}] ${createdBy}:**\n`;
|
|
232
|
+
contextText += `${note.note || "N/A"}\n\n`;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Public notes
|
|
237
|
+
if (publicNotes.length > 0) {
|
|
238
|
+
contextText += "# Public Notes\n\n";
|
|
239
|
+
for (const note of publicNotes) {
|
|
240
|
+
const noteTime: string = note.postedAt
|
|
241
|
+
? OneUptimeDate.getDateAsFormattedString(note.postedAt)
|
|
242
|
+
: note.createdAt
|
|
243
|
+
? OneUptimeDate.getDateAsFormattedString(note.createdAt)
|
|
244
|
+
: "N/A";
|
|
245
|
+
const createdBy: string =
|
|
246
|
+
note.createdByUser?.name?.toString() ||
|
|
247
|
+
note.createdByUser?.email?.toString() ||
|
|
248
|
+
"Unknown";
|
|
249
|
+
|
|
250
|
+
contextText += `**[${noteTime}] ${createdBy}:**\n`;
|
|
251
|
+
contextText += `${note.note || "N/A"}\n\n`;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// System prompt for note generation
|
|
256
|
+
let systemPrompt: string;
|
|
257
|
+
|
|
258
|
+
if (noteType === "public") {
|
|
259
|
+
if (template) {
|
|
260
|
+
systemPrompt = `You are an expert technical communicator. Your task is to fill in a public scheduled maintenance note template based on the provided maintenance event data.
|
|
261
|
+
|
|
262
|
+
CRITICAL INSTRUCTIONS:
|
|
263
|
+
- You MUST use ONLY the exact template structure provided below
|
|
264
|
+
- Fill in each section of the template with relevant information from the maintenance data
|
|
265
|
+
- Do NOT add any new sections, headers, or content that is not part of the template
|
|
266
|
+
- Do NOT add introductions, conclusions, or any text outside the template structure
|
|
267
|
+
- Write in a professional, clear, and customer-friendly manner
|
|
268
|
+
- Focus on what customers need to know: timing, impact, and what to expect
|
|
269
|
+
- Avoid technical jargon - keep it understandable for non-technical readers
|
|
270
|
+
- Be concise but informative
|
|
271
|
+
|
|
272
|
+
TEMPLATE TO FILL (use this exact structure):
|
|
273
|
+
|
|
274
|
+
${template}`;
|
|
275
|
+
} else {
|
|
276
|
+
systemPrompt = `You are an expert technical communicator. Your task is to generate a public scheduled maintenance note that will be visible to customers on the status page.
|
|
277
|
+
|
|
278
|
+
The note should:
|
|
279
|
+
1. Be written in a professional, customer-friendly tone
|
|
280
|
+
2. Clearly communicate the current status of the maintenance
|
|
281
|
+
3. Explain what work is being done and any impact on users
|
|
282
|
+
4. Provide timing information (when it started, expected completion, etc.)
|
|
283
|
+
5. Set appropriate expectations
|
|
284
|
+
6. Be concise but informative
|
|
285
|
+
|
|
286
|
+
DO NOT include:
|
|
287
|
+
- Internal technical details that customers don't need
|
|
288
|
+
- Confidential information
|
|
289
|
+
- Excessive jargon
|
|
290
|
+
|
|
291
|
+
Write in markdown format for better readability.`;
|
|
292
|
+
}
|
|
293
|
+
} else if (template) {
|
|
294
|
+
// Internal note with template
|
|
295
|
+
systemPrompt = `You are an expert Site Reliability Engineer (SRE). Your task is to fill in an internal scheduled maintenance note template based on the provided maintenance event data.
|
|
296
|
+
|
|
297
|
+
CRITICAL INSTRUCTIONS:
|
|
298
|
+
- You MUST use ONLY the exact template structure provided below
|
|
299
|
+
- Fill in each section of the template with relevant information from the maintenance data
|
|
300
|
+
- Do NOT add any new sections, headers, or content that is not part of the template
|
|
301
|
+
- Do NOT add introductions, conclusions, or any text outside the template structure
|
|
302
|
+
- Be technical and detailed - this is for the internal team
|
|
303
|
+
- Include relevant technical details, progress updates, and observations
|
|
304
|
+
|
|
305
|
+
TEMPLATE TO FILL (use this exact structure):
|
|
306
|
+
|
|
307
|
+
${template}`;
|
|
308
|
+
} else {
|
|
309
|
+
// Internal note without template
|
|
310
|
+
systemPrompt = `You are an expert Site Reliability Engineer (SRE). Your task is to generate an internal scheduled maintenance note for the team.
|
|
311
|
+
|
|
312
|
+
The note should:
|
|
313
|
+
1. Provide technical details about the maintenance progress
|
|
314
|
+
2. Document observations, findings, or actions taken
|
|
315
|
+
3. Include any issues encountered or changes to the plan
|
|
316
|
+
4. Be detailed enough to help team members understand the current status
|
|
317
|
+
5. Use technical language appropriate for the engineering team
|
|
318
|
+
|
|
319
|
+
Write in markdown format for better readability. Be thorough and technical.`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Build user message
|
|
323
|
+
const userMessage: string = template
|
|
324
|
+
? `Fill in the template above using ONLY the following scheduled maintenance data. Output only the filled template, nothing else:\n\n${contextText}`
|
|
325
|
+
: `Based on the following scheduled maintenance data, please generate ${noteType === "public" ? "a customer-facing public" : "an internal technical"} maintenance note:\n\n${contextText}`;
|
|
326
|
+
|
|
327
|
+
// Build messages array
|
|
328
|
+
const messages: Array<LLMMessage> = [
|
|
329
|
+
{
|
|
330
|
+
role: "system",
|
|
331
|
+
content: systemPrompt,
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
role: "user",
|
|
335
|
+
content: userMessage,
|
|
336
|
+
},
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
contextText,
|
|
341
|
+
systemPrompt,
|
|
342
|
+
messages,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
@@ -13,22 +13,43 @@ describe("class Domain", () => {
|
|
|
13
13
|
expect(new Domain("example.ac").domain).toBe("example.ac");
|
|
14
14
|
});
|
|
15
15
|
test("new Domain() should throw the BadDataException if domain is invalid", () => {
|
|
16
|
+
// No dot in domain
|
|
16
17
|
expect(() => {
|
|
17
18
|
return new Domain("example");
|
|
18
19
|
}).toThrowError(BadDataException);
|
|
19
20
|
expect(() => {
|
|
20
21
|
new Domain("example");
|
|
21
22
|
}).toThrowError(BadDataException);
|
|
23
|
+
|
|
24
|
+
// Invalid characters
|
|
22
25
|
expect(() => {
|
|
23
26
|
new Domain("example@com");
|
|
24
27
|
}).toThrowError(BadDataException);
|
|
25
28
|
|
|
29
|
+
// TLD with numbers (invalid - TLD must be letters only)
|
|
30
|
+
expect(() => {
|
|
31
|
+
new Domain("example.c0m");
|
|
32
|
+
}).toThrowError(BadDataException);
|
|
33
|
+
|
|
34
|
+
// Single letter TLD (invalid - TLD must be at least 2 characters)
|
|
26
35
|
expect(() => {
|
|
27
|
-
new Domain("example.
|
|
36
|
+
new Domain("example.c");
|
|
28
37
|
}).toThrowError(BadDataException);
|
|
38
|
+
|
|
39
|
+
// Domain starting with hyphen
|
|
40
|
+
expect(() => {
|
|
41
|
+
new Domain("-example.com");
|
|
42
|
+
}).toThrowError(BadDataException);
|
|
43
|
+
|
|
44
|
+
// Domain ending with hyphen before TLD
|
|
45
|
+
expect(() => {
|
|
46
|
+
new Domain("example-.com");
|
|
47
|
+
}).toThrowError(BadDataException);
|
|
48
|
+
|
|
49
|
+
// Mutation to invalid domain
|
|
29
50
|
expect(() => {
|
|
30
|
-
const validDomain: Domain = new Domain("example.
|
|
31
|
-
validDomain.domain = "
|
|
51
|
+
const validDomain: Domain = new Domain("example.com");
|
|
52
|
+
validDomain.domain = "invalid";
|
|
32
53
|
}).toThrowError(BadDataException);
|
|
33
54
|
});
|
|
34
55
|
test("Domain.domain should be mutable", () => {
|
package/Types/Domain.ts
CHANGED
|
@@ -18,33 +18,30 @@ export default class Domain extends DatabaseProperty {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
public static isValidDomain(domain: string): boolean {
|
|
21
|
-
|
|
21
|
+
/*
|
|
22
|
+
* Regex-based domain validation
|
|
23
|
+
* - Each label (part between dots) must be 1-63 characters
|
|
24
|
+
* - Labels can contain alphanumeric characters and hyphens
|
|
25
|
+
* - Labels cannot start or end with a hyphen
|
|
26
|
+
* - TLD must be at least 2 characters and contain only letters
|
|
27
|
+
* - Total length should not exceed 253 characters
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
if (!domain || domain.length > 253) {
|
|
22
31
|
return false;
|
|
23
32
|
}
|
|
24
33
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const beforeLastItem: string = parts[parts.length - 2] as string;
|
|
37
|
-
|
|
38
|
-
if (firstTLDs.includes(lastItem)) {
|
|
39
|
-
if (secondTLDs.includes(beforeLastItem)) {
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
|
-
return true;
|
|
43
|
-
} else if (secondTLDs.includes(lastItem)) {
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return false;
|
|
34
|
+
/*
|
|
35
|
+
* Domain validation regex:
|
|
36
|
+
* ^ - start of string
|
|
37
|
+
* (?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+ - one or more labels followed by dot
|
|
38
|
+
* [a-zA-Z]{2,63} - TLD: 2-63 letters only
|
|
39
|
+
* $ - end of string
|
|
40
|
+
*/
|
|
41
|
+
const domainRegex: RegExp =
|
|
42
|
+
/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}$/;
|
|
43
|
+
|
|
44
|
+
return domainRegex.test(domain);
|
|
48
45
|
}
|
|
49
46
|
|
|
50
47
|
public constructor(domain: string) {
|
|
@@ -20,6 +20,7 @@ export interface GenerateFromAIModalProps {
|
|
|
20
20
|
onGenerate: (data: GenerateAIRequestData) => Promise<string>;
|
|
21
21
|
onSuccess: (generatedContent: string) => void;
|
|
22
22
|
templates?: Array<{ id: string; name: string; content?: string }>;
|
|
23
|
+
noteType?: NoteType; // Type of note being generated (determines default templates)
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export interface GenerateAIRequestData {
|
|
@@ -27,13 +28,19 @@ export interface GenerateAIRequestData {
|
|
|
27
28
|
templateId?: string;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
// Template categories for different note types
|
|
32
|
+
export type NoteType = "postmortem" | "public-note" | "internal-note";
|
|
33
|
+
|
|
30
34
|
// Default hardcoded templates for incident postmortem
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
const POSTMORTEM_TEMPLATES: Array<{
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
content: string;
|
|
39
|
+
}> = [
|
|
40
|
+
{
|
|
41
|
+
id: "default-standard",
|
|
42
|
+
name: "Standard Postmortem",
|
|
43
|
+
content: `## Executive Summary
|
|
37
44
|
[Brief overview of the incident, its impact, and resolution]
|
|
38
45
|
|
|
39
46
|
## Incident Timeline
|
|
@@ -59,11 +66,11 @@ const DEFAULT_TEMPLATES: Array<{ id: string; name: string; content: string }> =
|
|
|
59
66
|
|
|
60
67
|
## Lessons Learned
|
|
61
68
|
[Key takeaways and improvements identified]`,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "default-detailed",
|
|
72
|
+
name: "Detailed Technical Postmortem",
|
|
73
|
+
content: `## Incident Overview
|
|
67
74
|
**Incident Title**: [Title]
|
|
68
75
|
**Severity**: [P1/P2/P3/P4]
|
|
69
76
|
**Duration**: [Start time] - [End time]
|
|
@@ -125,11 +132,11 @@ const DEFAULT_TEMPLATES: Array<{ id: string; name: string; content: string }> =
|
|
|
125
132
|
|
|
126
133
|
## Appendix
|
|
127
134
|
[Any additional technical details, logs, or graphs]`,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "default-brief",
|
|
138
|
+
name: "Brief Postmortem",
|
|
139
|
+
content: `## What Happened
|
|
133
140
|
[Concise description of the incident]
|
|
134
141
|
|
|
135
142
|
## Why It Happened
|
|
@@ -141,22 +148,152 @@ const DEFAULT_TEMPLATES: Array<{ id: string; name: string; content: string }> =
|
|
|
141
148
|
## How We Prevent It
|
|
142
149
|
- [ ] [Prevention action 1]
|
|
143
150
|
- [ ] [Prevention action 2]`,
|
|
144
|
-
|
|
145
|
-
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
// Default templates for public notes (customer-facing)
|
|
155
|
+
const PUBLIC_NOTE_TEMPLATES: Array<{
|
|
156
|
+
id: string;
|
|
157
|
+
name: string;
|
|
158
|
+
content: string;
|
|
159
|
+
}> = [
|
|
160
|
+
{
|
|
161
|
+
id: "public-status-update",
|
|
162
|
+
name: "Status Update",
|
|
163
|
+
content: `## Current Status
|
|
164
|
+
[Brief description of the current situation]
|
|
165
|
+
|
|
166
|
+
## What We're Doing
|
|
167
|
+
[Actions being taken to resolve the issue]
|
|
168
|
+
|
|
169
|
+
## Next Update
|
|
170
|
+
[Expected time for next update or resolution]`,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: "public-resolution",
|
|
174
|
+
name: "Resolution Notice",
|
|
175
|
+
content: `## Issue Resolved
|
|
176
|
+
[Brief description of what was resolved]
|
|
177
|
+
|
|
178
|
+
## Summary
|
|
179
|
+
[What happened and how it was fixed]
|
|
180
|
+
|
|
181
|
+
## Prevention
|
|
182
|
+
[Steps taken to prevent recurrence]
|
|
183
|
+
|
|
184
|
+
Thank you for your patience.`,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: "public-maintenance",
|
|
188
|
+
name: "Maintenance Update",
|
|
189
|
+
content: `## Maintenance Status
|
|
190
|
+
[Current phase of the maintenance]
|
|
191
|
+
|
|
192
|
+
## Progress
|
|
193
|
+
[What has been completed]
|
|
194
|
+
|
|
195
|
+
## Remaining Work
|
|
196
|
+
[What still needs to be done]
|
|
197
|
+
|
|
198
|
+
## Expected Completion
|
|
199
|
+
[Estimated completion time]`,
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
// Default templates for internal notes (team-facing)
|
|
204
|
+
const INTERNAL_NOTE_TEMPLATES: Array<{
|
|
205
|
+
id: string;
|
|
206
|
+
name: string;
|
|
207
|
+
content: string;
|
|
208
|
+
}> = [
|
|
209
|
+
{
|
|
210
|
+
id: "internal-investigation",
|
|
211
|
+
name: "Investigation Update",
|
|
212
|
+
content: `## Current Investigation Status
|
|
213
|
+
[What we're looking at]
|
|
214
|
+
|
|
215
|
+
## Findings So Far
|
|
216
|
+
- [Finding 1]
|
|
217
|
+
- [Finding 2]
|
|
218
|
+
|
|
219
|
+
## Hypothesis
|
|
220
|
+
[Current theory about the root cause]
|
|
221
|
+
|
|
222
|
+
## Next Steps
|
|
223
|
+
- [ ] [Action 1]
|
|
224
|
+
- [ ] [Action 2]`,
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: "internal-technical",
|
|
228
|
+
name: "Technical Analysis",
|
|
229
|
+
content: `## Technical Details
|
|
230
|
+
[Detailed technical observations]
|
|
231
|
+
|
|
232
|
+
## Metrics/Logs
|
|
233
|
+
[Relevant metrics or log entries]
|
|
234
|
+
|
|
235
|
+
## Impact Assessment
|
|
236
|
+
[Technical impact analysis]
|
|
237
|
+
|
|
238
|
+
## Recommendations
|
|
239
|
+
[Technical recommendations for resolution]`,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: "internal-handoff",
|
|
243
|
+
name: "Shift Handoff",
|
|
244
|
+
content: `## Current State
|
|
245
|
+
[Where things stand now]
|
|
246
|
+
|
|
247
|
+
## Actions Taken
|
|
248
|
+
[What has been done so far]
|
|
249
|
+
|
|
250
|
+
## Open Questions
|
|
251
|
+
[Things that still need investigation]
|
|
252
|
+
|
|
253
|
+
## Immediate Priorities
|
|
254
|
+
- [ ] [Priority 1]
|
|
255
|
+
- [ ] [Priority 2]
|
|
256
|
+
|
|
257
|
+
## Contacts
|
|
258
|
+
[Key people involved or to contact]`,
|
|
259
|
+
},
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
// Function to get default templates based on note type
|
|
263
|
+
const getDefaultTemplates: (
|
|
264
|
+
noteType: NoteType,
|
|
265
|
+
) => Array<{ id: string; name: string; content: string }> = (
|
|
266
|
+
noteType: NoteType,
|
|
267
|
+
): Array<{ id: string; name: string; content: string }> => {
|
|
268
|
+
switch (noteType) {
|
|
269
|
+
case "postmortem":
|
|
270
|
+
return POSTMORTEM_TEMPLATES;
|
|
271
|
+
case "public-note":
|
|
272
|
+
return PUBLIC_NOTE_TEMPLATES;
|
|
273
|
+
case "internal-note":
|
|
274
|
+
return INTERNAL_NOTE_TEMPLATES;
|
|
275
|
+
default:
|
|
276
|
+
return POSTMORTEM_TEMPLATES;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
146
279
|
|
|
147
280
|
const GenerateFromAIModal: FunctionComponent<GenerateFromAIModalProps> = (
|
|
148
281
|
props: GenerateFromAIModalProps,
|
|
149
282
|
): ReactElement => {
|
|
283
|
+
// Get default templates based on note type
|
|
284
|
+
const defaultTemplates: Array<{ id: string; name: string; content: string }> =
|
|
285
|
+
getDefaultTemplates(props.noteType || "postmortem");
|
|
286
|
+
|
|
150
287
|
const [isGenerating, setIsGenerating] = useState<boolean>(false);
|
|
151
288
|
const [error, setError] = useState<string>("");
|
|
152
289
|
const [selectedTemplateId, setSelectedTemplateId] = useState<string>(
|
|
153
|
-
|
|
290
|
+
defaultTemplates[0]?.id || "",
|
|
154
291
|
);
|
|
155
292
|
const [templateContent, setTemplateContent] = useState<string>("");
|
|
156
293
|
|
|
157
294
|
// Combine default templates with custom templates
|
|
158
295
|
const allTemplates: Array<{ id: string; name: string; content?: string }> = [
|
|
159
|
-
...
|
|
296
|
+
...defaultTemplates,
|
|
160
297
|
...(props.templates || []),
|
|
161
298
|
];
|
|
162
299
|
|