@equilateral_ai/mindmeld 3.0.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/README.md +300 -0
- package/hooks/README.md +494 -0
- package/hooks/pre-compact.js +392 -0
- package/hooks/session-start.js +264 -0
- package/package.json +90 -0
- package/scripts/harvest.js +561 -0
- package/scripts/init-project.js +437 -0
- package/scripts/inject.js +388 -0
- package/src/collaboration/CollaborationPrompt.js +460 -0
- package/src/core/AlertEngine.js +813 -0
- package/src/core/AlertNotifier.js +363 -0
- package/src/core/CorrelationAnalyzer.js +774 -0
- package/src/core/CurationEngine.js +688 -0
- package/src/core/LLMPatternDetector.js +508 -0
- package/src/core/LoadBearingDetector.js +242 -0
- package/src/core/NotificationService.js +1032 -0
- package/src/core/PatternValidator.js +355 -0
- package/src/core/README.md +160 -0
- package/src/core/RapportOrchestrator.js +446 -0
- package/src/core/RelevanceDetector.js +577 -0
- package/src/core/StandardsIngestion.js +575 -0
- package/src/core/TeamLoadBearingDetector.js +431 -0
- package/src/database/dbOperations.js +105 -0
- package/src/handlers/activity/activityGetMe.js +98 -0
- package/src/handlers/activity/activityGetTeam.js +130 -0
- package/src/handlers/alerts/alertsAcknowledge.js +91 -0
- package/src/handlers/alerts/alertsGet.js +250 -0
- package/src/handlers/collaborators/collaboratorAdd.js +201 -0
- package/src/handlers/collaborators/collaboratorInvite.js +218 -0
- package/src/handlers/collaborators/collaboratorList.js +88 -0
- package/src/handlers/collaborators/collaboratorRemove.js +127 -0
- package/src/handlers/collaborators/inviteAccept.js +122 -0
- package/src/handlers/context/contextGet.js +57 -0
- package/src/handlers/context/invariantsGet.js +74 -0
- package/src/handlers/context/loopsGet.js +82 -0
- package/src/handlers/context/notesCreate.js +74 -0
- package/src/handlers/context/purposeGet.js +78 -0
- package/src/handlers/correlations/correlationsDeveloperGet.js +226 -0
- package/src/handlers/correlations/correlationsGet.js +93 -0
- package/src/handlers/correlations/correlationsProjectGet.js +161 -0
- package/src/handlers/github/githubConnectionStatus.js +49 -0
- package/src/handlers/github/githubDiscoverPatterns.js +364 -0
- package/src/handlers/github/githubOAuthCallback.js +166 -0
- package/src/handlers/github/githubOAuthStart.js +59 -0
- package/src/handlers/github/githubPatternsReview.js +109 -0
- package/src/handlers/github/githubReposList.js +105 -0
- package/src/handlers/helpers/checkSuperAdmin.js +85 -0
- package/src/handlers/helpers/dbOperations.js +53 -0
- package/src/handlers/helpers/errorHandler.js +49 -0
- package/src/handlers/helpers/index.js +106 -0
- package/src/handlers/helpers/lambdaWrapper.js +60 -0
- package/src/handlers/helpers/responseUtil.js +55 -0
- package/src/handlers/helpers/subscriptionTiers.js +1168 -0
- package/src/handlers/notifications/getPreferences.js +84 -0
- package/src/handlers/notifications/sendNotification.js +170 -0
- package/src/handlers/notifications/updatePreferences.js +316 -0
- package/src/handlers/patterns/patternUsagePost.js +182 -0
- package/src/handlers/patterns/patternViolationPost.js +185 -0
- package/src/handlers/projects/projectCreate.js +107 -0
- package/src/handlers/projects/projectDelete.js +82 -0
- package/src/handlers/projects/projectGet.js +95 -0
- package/src/handlers/projects/projectUpdate.js +118 -0
- package/src/handlers/reports/aiLeverage.js +206 -0
- package/src/handlers/reports/engineeringInvestment.js +132 -0
- package/src/handlers/reports/riskForecast.js +186 -0
- package/src/handlers/reports/standardsRoi.js +162 -0
- package/src/handlers/scheduled/analyzeCorrelations.js +178 -0
- package/src/handlers/scheduled/analyzeGitHistory.js +510 -0
- package/src/handlers/scheduled/generateAlerts.js +135 -0
- package/src/handlers/scheduled/refreshActivity.js +21 -0
- package/src/handlers/scheduled/scanCompliance.js +334 -0
- package/src/handlers/sessions/sessionEndPost.js +180 -0
- package/src/handlers/sessions/sessionStandardsPost.js +135 -0
- package/src/handlers/stripe/addonManagePost.js +240 -0
- package/src/handlers/stripe/billingPortalPost.js +93 -0
- package/src/handlers/stripe/enterpriseCheckoutPost.js +272 -0
- package/src/handlers/stripe/seatsUpdatePost.js +185 -0
- package/src/handlers/stripe/subscriptionCancelDelete.js +169 -0
- package/src/handlers/stripe/subscriptionCreatePost.js +221 -0
- package/src/handlers/stripe/subscriptionUpdatePut.js +163 -0
- package/src/handlers/stripe/webhookPost.js +454 -0
- package/src/handlers/users/cognitoPostConfirmation.js +150 -0
- package/src/handlers/users/userEntitlementsGet.js +89 -0
- package/src/handlers/users/userGet.js +114 -0
- package/src/handlers/webhooks/githubWebhook.js +223 -0
- package/src/index.js +969 -0
|
@@ -0,0 +1,1032 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rapport v3 - Notification Service
|
|
3
|
+
*
|
|
4
|
+
* Purpose: Sends notifications via email (SES) and Slack webhooks
|
|
5
|
+
*
|
|
6
|
+
* Notification Types:
|
|
7
|
+
* - Pattern promotions (pattern validated/reinforced)
|
|
8
|
+
* - Weekly digests (team activity summary)
|
|
9
|
+
* - Critical violations (standards violations)
|
|
10
|
+
* - Team alerts (attention alerts for managers)
|
|
11
|
+
* - Curation candidates (patterns ready for review)
|
|
12
|
+
*
|
|
13
|
+
* Based on: Tim-Combo pattern with environment-based configuration
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { SESClient, SendEmailCommand, SendTemplatedEmailCommand } = require('@aws-sdk/client-ses');
|
|
17
|
+
|
|
18
|
+
class NotificationService {
|
|
19
|
+
constructor(config = {}) {
|
|
20
|
+
this.config = {
|
|
21
|
+
sesRegion: config.sesRegion || process.env.AWS_REGION || 'us-east-2',
|
|
22
|
+
fromEmail: config.fromEmail || process.env.SES_FROM_EMAIL || 'noreply@mindmeld.dev',
|
|
23
|
+
fromName: config.fromName || 'MindMeld',
|
|
24
|
+
slackWebhookUrl: config.slackWebhookUrl || process.env.SLACK_WEBHOOK_URL,
|
|
25
|
+
slackCriticalWebhookUrl: config.slackCriticalWebhookUrl || process.env.SLACK_CRITICAL_WEBHOOK_URL,
|
|
26
|
+
appUrl: config.appUrl || process.env.APP_URL || 'https://mindmeld.dev',
|
|
27
|
+
enabled: config.enabled !== false && process.env.NOTIFICATIONS_ENABLED !== 'false',
|
|
28
|
+
...config
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Initialize SES client (lazy - created once, reused across invocations)
|
|
32
|
+
this.sesClient = null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get SES client (cached single client pattern for Lambda)
|
|
37
|
+
*/
|
|
38
|
+
getSESClient() {
|
|
39
|
+
if (!this.sesClient) {
|
|
40
|
+
this.sesClient = new SESClient({ region: this.config.sesRegion });
|
|
41
|
+
}
|
|
42
|
+
return this.sesClient;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Send a notification based on type and preferences
|
|
47
|
+
*
|
|
48
|
+
* @param {Object} options - Notification options
|
|
49
|
+
* @param {string} options.type - Notification type (pattern_promotion, weekly_digest, critical_violation, team_alert, curation_candidate)
|
|
50
|
+
* @param {string} options.email - Recipient email
|
|
51
|
+
* @param {Object} options.preferences - User notification preferences
|
|
52
|
+
* @param {Object} options.data - Notification-specific data
|
|
53
|
+
* @param {string} options.projectId - Optional project ID for project-specific preferences
|
|
54
|
+
* @returns {Promise<Object>} Send result
|
|
55
|
+
*/
|
|
56
|
+
async sendNotification({ type, email, preferences, data, projectId }) {
|
|
57
|
+
if (!this.config.enabled) {
|
|
58
|
+
console.log('[NotificationService] Notifications disabled, skipping');
|
|
59
|
+
return { sent: false, reason: 'notifications_disabled' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const results = { email: null, slack: null };
|
|
63
|
+
|
|
64
|
+
// Check email preferences
|
|
65
|
+
if (this.shouldSendEmail(type, preferences, projectId)) {
|
|
66
|
+
try {
|
|
67
|
+
results.email = await this.sendEmail(type, email, data);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('[NotificationService] Email send failed:', error);
|
|
70
|
+
results.email = { sent: false, error: error.message };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check Slack preferences (for team/project notifications)
|
|
75
|
+
if (this.shouldSendSlack(type, preferences, projectId, data)) {
|
|
76
|
+
try {
|
|
77
|
+
results.slack = await this.sendSlack(type, data);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('[NotificationService] Slack send failed:', error);
|
|
80
|
+
results.slack = { sent: false, error: error.message };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return results;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if email notification should be sent
|
|
89
|
+
*/
|
|
90
|
+
shouldSendEmail(type, preferences, projectId) {
|
|
91
|
+
if (!preferences) return true; // Default to enabled if no preferences
|
|
92
|
+
|
|
93
|
+
// Check global email preference
|
|
94
|
+
if (preferences.email_enabled === false) return false;
|
|
95
|
+
|
|
96
|
+
// Check type-specific preference
|
|
97
|
+
const typePrefs = preferences.notification_types || {};
|
|
98
|
+
if (typePrefs[type]?.email === false) return false;
|
|
99
|
+
|
|
100
|
+
// Check project-specific preference
|
|
101
|
+
if (projectId && preferences.project_overrides) {
|
|
102
|
+
const projectPrefs = preferences.project_overrides[projectId];
|
|
103
|
+
if (projectPrefs?.email_enabled === false) return false;
|
|
104
|
+
if (projectPrefs?.notification_types?.[type]?.email === false) return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check if Slack notification should be sent
|
|
112
|
+
*/
|
|
113
|
+
shouldSendSlack(type, preferences, projectId, data) {
|
|
114
|
+
// Must have webhook URL configured
|
|
115
|
+
const isCritical = type === 'critical_violation' || data?.severity === 'critical';
|
|
116
|
+
const webhookUrl = isCritical ? this.config.slackCriticalWebhookUrl : this.config.slackWebhookUrl;
|
|
117
|
+
|
|
118
|
+
if (!webhookUrl) return false;
|
|
119
|
+
|
|
120
|
+
if (!preferences) return true; // Default to enabled if no preferences
|
|
121
|
+
|
|
122
|
+
// Check global Slack preference
|
|
123
|
+
if (preferences.slack_enabled === false) return false;
|
|
124
|
+
|
|
125
|
+
// Check type-specific preference
|
|
126
|
+
const typePrefs = preferences.notification_types || {};
|
|
127
|
+
if (typePrefs[type]?.slack === false) return false;
|
|
128
|
+
|
|
129
|
+
// Check project-specific preference
|
|
130
|
+
if (projectId && preferences.project_overrides) {
|
|
131
|
+
const projectPrefs = preferences.project_overrides[projectId];
|
|
132
|
+
if (projectPrefs?.slack_enabled === false) return false;
|
|
133
|
+
if (projectPrefs?.notification_types?.[type]?.slack === false) return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Send email notification via SES
|
|
141
|
+
*/
|
|
142
|
+
async sendEmail(type, email, data) {
|
|
143
|
+
const emailContent = this.buildEmailContent(type, data);
|
|
144
|
+
|
|
145
|
+
const command = new SendEmailCommand({
|
|
146
|
+
Source: `${this.config.fromName} <${this.config.fromEmail}>`,
|
|
147
|
+
Destination: {
|
|
148
|
+
ToAddresses: [email]
|
|
149
|
+
},
|
|
150
|
+
Message: {
|
|
151
|
+
Subject: {
|
|
152
|
+
Data: emailContent.subject,
|
|
153
|
+
Charset: 'UTF-8'
|
|
154
|
+
},
|
|
155
|
+
Body: {
|
|
156
|
+
Html: {
|
|
157
|
+
Data: emailContent.html,
|
|
158
|
+
Charset: 'UTF-8'
|
|
159
|
+
},
|
|
160
|
+
Text: {
|
|
161
|
+
Data: emailContent.text,
|
|
162
|
+
Charset: 'UTF-8'
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const result = await this.getSESClient().send(command);
|
|
169
|
+
|
|
170
|
+
console.log(`[NotificationService] Email sent: ${type} to ${email}`);
|
|
171
|
+
return { sent: true, messageId: result.MessageId };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Build email content based on notification type
|
|
176
|
+
*/
|
|
177
|
+
buildEmailContent(type, data) {
|
|
178
|
+
switch (type) {
|
|
179
|
+
case 'pattern_promotion':
|
|
180
|
+
return this.buildPatternPromotionEmail(data);
|
|
181
|
+
case 'weekly_digest':
|
|
182
|
+
return this.buildWeeklyDigestEmail(data);
|
|
183
|
+
case 'critical_violation':
|
|
184
|
+
return this.buildCriticalViolationEmail(data);
|
|
185
|
+
case 'team_alert':
|
|
186
|
+
return this.buildTeamAlertEmail(data);
|
|
187
|
+
case 'curation_candidate':
|
|
188
|
+
return this.buildCurationCandidateEmail(data);
|
|
189
|
+
default:
|
|
190
|
+
return this.buildGenericEmail(type, data);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Build pattern promotion email
|
|
196
|
+
*/
|
|
197
|
+
buildPatternPromotionEmail(data) {
|
|
198
|
+
const { patternName, projectName, newMaturity, evidence } = data;
|
|
199
|
+
|
|
200
|
+
const subject = `Pattern Promoted: ${patternName}`;
|
|
201
|
+
|
|
202
|
+
const html = `
|
|
203
|
+
<!DOCTYPE html>
|
|
204
|
+
<html>
|
|
205
|
+
<head>
|
|
206
|
+
<style>
|
|
207
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
|
208
|
+
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
209
|
+
.header { background: #2563eb; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
|
|
210
|
+
.content { background: #f8fafc; padding: 20px; border-radius: 0 0 8px 8px; }
|
|
211
|
+
.metric { display: inline-block; margin: 10px 20px 10px 0; }
|
|
212
|
+
.metric-value { font-size: 24px; font-weight: bold; color: #2563eb; }
|
|
213
|
+
.metric-label { font-size: 12px; color: #64748b; }
|
|
214
|
+
.button { display: inline-block; background: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; margin-top: 20px; }
|
|
215
|
+
.footer { margin-top: 20px; font-size: 12px; color: #94a3b8; }
|
|
216
|
+
</style>
|
|
217
|
+
</head>
|
|
218
|
+
<body>
|
|
219
|
+
<div class="container">
|
|
220
|
+
<div class="header">
|
|
221
|
+
<h1>Pattern Promoted</h1>
|
|
222
|
+
</div>
|
|
223
|
+
<div class="content">
|
|
224
|
+
<h2>${patternName}</h2>
|
|
225
|
+
<p>A pattern in <strong>${projectName}</strong> has been promoted to <strong>${newMaturity}</strong> status.</p>
|
|
226
|
+
|
|
227
|
+
${evidence ? `
|
|
228
|
+
<div class="metrics">
|
|
229
|
+
<div class="metric">
|
|
230
|
+
<div class="metric-value">${(evidence.correlation * 100).toFixed(0)}%</div>
|
|
231
|
+
<div class="metric-label">Success Rate</div>
|
|
232
|
+
</div>
|
|
233
|
+
<div class="metric">
|
|
234
|
+
<div class="metric-value">${evidence.projectCount || 0}</div>
|
|
235
|
+
<div class="metric-label">Projects</div>
|
|
236
|
+
</div>
|
|
237
|
+
<div class="metric">
|
|
238
|
+
<div class="metric-value">${evidence.developerCount || 0}</div>
|
|
239
|
+
<div class="metric-label">Developers</div>
|
|
240
|
+
</div>
|
|
241
|
+
<div class="metric">
|
|
242
|
+
<div class="metric-value">${evidence.sessionCount || 0}</div>
|
|
243
|
+
<div class="metric-label">Sessions</div>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
` : ''}
|
|
247
|
+
|
|
248
|
+
<a href="${this.config.appUrl}/dashboard/patterns" class="button">View Patterns</a>
|
|
249
|
+
</div>
|
|
250
|
+
<div class="footer">
|
|
251
|
+
<p>You received this because you're a collaborator on ${projectName}.</p>
|
|
252
|
+
<p><a href="${this.config.appUrl}/settings/notifications">Manage notification preferences</a></p>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
</body>
|
|
256
|
+
</html>`;
|
|
257
|
+
|
|
258
|
+
const text = `Pattern Promoted: ${patternName}
|
|
259
|
+
|
|
260
|
+
A pattern in ${projectName} has been promoted to ${newMaturity} status.
|
|
261
|
+
|
|
262
|
+
${evidence ? `Evidence:
|
|
263
|
+
- Success Rate: ${(evidence.correlation * 100).toFixed(0)}%
|
|
264
|
+
- Projects: ${evidence.projectCount || 0}
|
|
265
|
+
- Developers: ${evidence.developerCount || 0}
|
|
266
|
+
- Sessions: ${evidence.sessionCount || 0}` : ''}
|
|
267
|
+
|
|
268
|
+
View patterns: ${this.config.appUrl}/dashboard/patterns
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
Manage notification preferences: ${this.config.appUrl}/settings/notifications`;
|
|
272
|
+
|
|
273
|
+
return { subject, html, text };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Build weekly digest email
|
|
278
|
+
*/
|
|
279
|
+
buildWeeklyDigestEmail(data) {
|
|
280
|
+
const {
|
|
281
|
+
userName,
|
|
282
|
+
weekStart,
|
|
283
|
+
weekEnd,
|
|
284
|
+
patternsLearned,
|
|
285
|
+
patternsReinforced,
|
|
286
|
+
sessionsCount,
|
|
287
|
+
teamActivity,
|
|
288
|
+
loadBearingElements
|
|
289
|
+
} = data;
|
|
290
|
+
|
|
291
|
+
const subject = `Weekly Digest: ${weekStart} - ${weekEnd}`;
|
|
292
|
+
|
|
293
|
+
const html = `
|
|
294
|
+
<!DOCTYPE html>
|
|
295
|
+
<html>
|
|
296
|
+
<head>
|
|
297
|
+
<style>
|
|
298
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
|
299
|
+
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
300
|
+
.header { background: linear-gradient(135deg, #2563eb, #7c3aed); color: white; padding: 20px; border-radius: 8px 8px 0 0; }
|
|
301
|
+
.content { background: #f8fafc; padding: 20px; }
|
|
302
|
+
.section { background: white; padding: 16px; border-radius: 8px; margin-bottom: 16px; }
|
|
303
|
+
.section h3 { margin-top: 0; color: #1e293b; }
|
|
304
|
+
.stat-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #e2e8f0; }
|
|
305
|
+
.stat-row:last-child { border-bottom: none; }
|
|
306
|
+
.stat-label { color: #64748b; }
|
|
307
|
+
.stat-value { font-weight: 600; color: #1e293b; }
|
|
308
|
+
.footer { margin-top: 20px; font-size: 12px; color: #94a3b8; text-align: center; }
|
|
309
|
+
</style>
|
|
310
|
+
</head>
|
|
311
|
+
<body>
|
|
312
|
+
<div class="container">
|
|
313
|
+
<div class="header">
|
|
314
|
+
<h1>Weekly Digest</h1>
|
|
315
|
+
<p>${weekStart} - ${weekEnd}</p>
|
|
316
|
+
</div>
|
|
317
|
+
<div class="content">
|
|
318
|
+
<p>Hi ${userName},</p>
|
|
319
|
+
<p>Here's your team's collaboration summary for the week.</p>
|
|
320
|
+
|
|
321
|
+
<div class="section">
|
|
322
|
+
<h3>Your Activity</h3>
|
|
323
|
+
<div class="stat-row">
|
|
324
|
+
<span class="stat-label">Sessions</span>
|
|
325
|
+
<span class="stat-value">${sessionsCount || 0}</span>
|
|
326
|
+
</div>
|
|
327
|
+
<div class="stat-row">
|
|
328
|
+
<span class="stat-label">Patterns Learned</span>
|
|
329
|
+
<span class="stat-value">${patternsLearned || 0}</span>
|
|
330
|
+
</div>
|
|
331
|
+
<div class="stat-row">
|
|
332
|
+
<span class="stat-label">Patterns Reinforced</span>
|
|
333
|
+
<span class="stat-value">${patternsReinforced || 0}</span>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
${teamActivity ? `
|
|
338
|
+
<div class="section">
|
|
339
|
+
<h3>Team Activity</h3>
|
|
340
|
+
<div class="stat-row">
|
|
341
|
+
<span class="stat-label">Total Sessions</span>
|
|
342
|
+
<span class="stat-value">${teamActivity.totalSessions || 0}</span>
|
|
343
|
+
</div>
|
|
344
|
+
<div class="stat-row">
|
|
345
|
+
<span class="stat-label">Active Collaborators</span>
|
|
346
|
+
<span class="stat-value">${teamActivity.activeCollaborators || 0}</span>
|
|
347
|
+
</div>
|
|
348
|
+
<div class="stat-row">
|
|
349
|
+
<span class="stat-label">Shared Patterns</span>
|
|
350
|
+
<span class="stat-value">${teamActivity.sharedPatterns || 0}</span>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
` : ''}
|
|
354
|
+
|
|
355
|
+
${loadBearingElements && loadBearingElements.length > 0 ? `
|
|
356
|
+
<div class="section">
|
|
357
|
+
<h3>Load-Bearing Context (Top ${loadBearingElements.length})</h3>
|
|
358
|
+
${loadBearingElements.map(el => `
|
|
359
|
+
<div class="stat-row">
|
|
360
|
+
<span class="stat-label">${el.key}</span>
|
|
361
|
+
<span class="stat-value">${(el.correlation * 100).toFixed(0)}% correlation</span>
|
|
362
|
+
</div>
|
|
363
|
+
`).join('')}
|
|
364
|
+
</div>
|
|
365
|
+
` : ''}
|
|
366
|
+
</div>
|
|
367
|
+
<div class="footer">
|
|
368
|
+
<p><a href="${this.config.appUrl}/dashboard">View Full Dashboard</a></p>
|
|
369
|
+
<p><a href="${this.config.appUrl}/settings/notifications">Manage notification preferences</a></p>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
</body>
|
|
373
|
+
</html>`;
|
|
374
|
+
|
|
375
|
+
const text = `Weekly Digest: ${weekStart} - ${weekEnd}
|
|
376
|
+
|
|
377
|
+
Hi ${userName},
|
|
378
|
+
|
|
379
|
+
Here's your team's collaboration summary for the week.
|
|
380
|
+
|
|
381
|
+
YOUR ACTIVITY
|
|
382
|
+
- Sessions: ${sessionsCount || 0}
|
|
383
|
+
- Patterns Learned: ${patternsLearned || 0}
|
|
384
|
+
- Patterns Reinforced: ${patternsReinforced || 0}
|
|
385
|
+
|
|
386
|
+
${teamActivity ? `TEAM ACTIVITY
|
|
387
|
+
- Total Sessions: ${teamActivity.totalSessions || 0}
|
|
388
|
+
- Active Collaborators: ${teamActivity.activeCollaborators || 0}
|
|
389
|
+
- Shared Patterns: ${teamActivity.sharedPatterns || 0}` : ''}
|
|
390
|
+
|
|
391
|
+
View dashboard: ${this.config.appUrl}/dashboard
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
Manage notification preferences: ${this.config.appUrl}/settings/notifications`;
|
|
395
|
+
|
|
396
|
+
return { subject, html, text };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Build critical violation email
|
|
401
|
+
*/
|
|
402
|
+
buildCriticalViolationEmail(data) {
|
|
403
|
+
const { violationType, projectName, standardName, details, filePath } = data;
|
|
404
|
+
|
|
405
|
+
const subject = `[CRITICAL] Standards Violation: ${standardName}`;
|
|
406
|
+
|
|
407
|
+
const html = `
|
|
408
|
+
<!DOCTYPE html>
|
|
409
|
+
<html>
|
|
410
|
+
<head>
|
|
411
|
+
<style>
|
|
412
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
|
413
|
+
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
414
|
+
.header { background: #dc2626; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
|
|
415
|
+
.content { background: #fef2f2; padding: 20px; border-radius: 0 0 8px 8px; }
|
|
416
|
+
.alert-box { background: white; border-left: 4px solid #dc2626; padding: 16px; margin: 16px 0; }
|
|
417
|
+
.code-block { background: #1e293b; color: #e2e8f0; padding: 16px; border-radius: 6px; font-family: monospace; overflow-x: auto; }
|
|
418
|
+
.button { display: inline-block; background: #dc2626; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; margin-top: 20px; }
|
|
419
|
+
.footer { margin-top: 20px; font-size: 12px; color: #94a3b8; }
|
|
420
|
+
</style>
|
|
421
|
+
</head>
|
|
422
|
+
<body>
|
|
423
|
+
<div class="container">
|
|
424
|
+
<div class="header">
|
|
425
|
+
<h1>Critical Standards Violation</h1>
|
|
426
|
+
</div>
|
|
427
|
+
<div class="content">
|
|
428
|
+
<div class="alert-box">
|
|
429
|
+
<strong>Standard:</strong> ${standardName}<br>
|
|
430
|
+
<strong>Project:</strong> ${projectName}<br>
|
|
431
|
+
${filePath ? `<strong>File:</strong> ${filePath}<br>` : ''}
|
|
432
|
+
<strong>Type:</strong> ${violationType}
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
<h3>Details</h3>
|
|
436
|
+
<p>${details}</p>
|
|
437
|
+
|
|
438
|
+
<a href="${this.config.appUrl}/dashboard/violations" class="button">View Violations</a>
|
|
439
|
+
</div>
|
|
440
|
+
<div class="footer">
|
|
441
|
+
<p>This is a critical alert. You received this because you're a project admin.</p>
|
|
442
|
+
<p><a href="${this.config.appUrl}/settings/notifications">Manage notification preferences</a></p>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
</body>
|
|
446
|
+
</html>`;
|
|
447
|
+
|
|
448
|
+
const text = `[CRITICAL] Standards Violation: ${standardName}
|
|
449
|
+
|
|
450
|
+
Standard: ${standardName}
|
|
451
|
+
Project: ${projectName}
|
|
452
|
+
${filePath ? `File: ${filePath}` : ''}
|
|
453
|
+
Type: ${violationType}
|
|
454
|
+
|
|
455
|
+
Details:
|
|
456
|
+
${details}
|
|
457
|
+
|
|
458
|
+
View violations: ${this.config.appUrl}/dashboard/violations
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
Manage notification preferences: ${this.config.appUrl}/settings/notifications`;
|
|
462
|
+
|
|
463
|
+
return { subject, html, text };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Build team alert email
|
|
468
|
+
*/
|
|
469
|
+
buildTeamAlertEmail(data) {
|
|
470
|
+
const { alertType, severity, userName, details, message } = data;
|
|
471
|
+
|
|
472
|
+
const severityColors = {
|
|
473
|
+
concern: '#dc2626',
|
|
474
|
+
warning: '#f59e0b',
|
|
475
|
+
info: '#3b82f6'
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const subject = `[${severity.toUpperCase()}] Team Alert: ${message}`;
|
|
479
|
+
|
|
480
|
+
const html = `
|
|
481
|
+
<!DOCTYPE html>
|
|
482
|
+
<html>
|
|
483
|
+
<head>
|
|
484
|
+
<style>
|
|
485
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
|
486
|
+
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
487
|
+
.header { background: ${severityColors[severity] || '#3b82f6'}; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
|
|
488
|
+
.content { background: #f8fafc; padding: 20px; border-radius: 0 0 8px 8px; }
|
|
489
|
+
.alert-box { background: white; border-left: 4px solid ${severityColors[severity] || '#3b82f6'}; padding: 16px; margin: 16px 0; }
|
|
490
|
+
.button { display: inline-block; background: ${severityColors[severity] || '#3b82f6'}; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; margin-top: 20px; }
|
|
491
|
+
.footer { margin-top: 20px; font-size: 12px; color: #94a3b8; }
|
|
492
|
+
</style>
|
|
493
|
+
</head>
|
|
494
|
+
<body>
|
|
495
|
+
<div class="container">
|
|
496
|
+
<div class="header">
|
|
497
|
+
<h1>Team Alert</h1>
|
|
498
|
+
</div>
|
|
499
|
+
<div class="content">
|
|
500
|
+
<div class="alert-box">
|
|
501
|
+
<strong>Team Member:</strong> ${userName}<br>
|
|
502
|
+
<strong>Alert Type:</strong> ${alertType}<br>
|
|
503
|
+
<strong>Severity:</strong> ${severity}
|
|
504
|
+
</div>
|
|
505
|
+
|
|
506
|
+
<h3>${message}</h3>
|
|
507
|
+
|
|
508
|
+
${details ? `
|
|
509
|
+
<ul>
|
|
510
|
+
${Object.entries(details).map(([key, value]) => `<li><strong>${key}:</strong> ${value}</li>`).join('')}
|
|
511
|
+
</ul>
|
|
512
|
+
` : ''}
|
|
513
|
+
|
|
514
|
+
<a href="${this.config.appUrl}/dashboard/alerts" class="button">View Alerts</a>
|
|
515
|
+
</div>
|
|
516
|
+
<div class="footer">
|
|
517
|
+
<p>You received this because you're a team manager.</p>
|
|
518
|
+
<p><a href="${this.config.appUrl}/settings/notifications">Manage notification preferences</a></p>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
</body>
|
|
522
|
+
</html>`;
|
|
523
|
+
|
|
524
|
+
const text = `[${severity.toUpperCase()}] Team Alert: ${message}
|
|
525
|
+
|
|
526
|
+
Team Member: ${userName}
|
|
527
|
+
Alert Type: ${alertType}
|
|
528
|
+
Severity: ${severity}
|
|
529
|
+
|
|
530
|
+
${details ? Object.entries(details).map(([key, value]) => `${key}: ${value}`).join('\n') : ''}
|
|
531
|
+
|
|
532
|
+
View alerts: ${this.config.appUrl}/dashboard/alerts
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
Manage notification preferences: ${this.config.appUrl}/settings/notifications`;
|
|
536
|
+
|
|
537
|
+
return { subject, html, text };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Build curation candidate email
|
|
542
|
+
*/
|
|
543
|
+
buildCurationCandidateEmail(data) {
|
|
544
|
+
const { candidateId, patternName, category, evidence } = data;
|
|
545
|
+
|
|
546
|
+
const subject = `New Curation Candidate: ${patternName}`;
|
|
547
|
+
|
|
548
|
+
const html = `
|
|
549
|
+
<!DOCTYPE html>
|
|
550
|
+
<html>
|
|
551
|
+
<head>
|
|
552
|
+
<style>
|
|
553
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
|
554
|
+
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
555
|
+
.header { background: linear-gradient(135deg, #059669, #10b981); color: white; padding: 20px; border-radius: 8px 8px 0 0; }
|
|
556
|
+
.content { background: #f0fdf4; padding: 20px; border-radius: 0 0 8px 8px; }
|
|
557
|
+
.metric { display: inline-block; margin: 10px 20px 10px 0; }
|
|
558
|
+
.metric-value { font-size: 24px; font-weight: bold; color: #059669; }
|
|
559
|
+
.metric-label { font-size: 12px; color: #64748b; }
|
|
560
|
+
.button { display: inline-block; background: #059669; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; margin-top: 20px; margin-right: 10px; }
|
|
561
|
+
.button-secondary { background: #64748b; }
|
|
562
|
+
.footer { margin-top: 20px; font-size: 12px; color: #94a3b8; }
|
|
563
|
+
</style>
|
|
564
|
+
</head>
|
|
565
|
+
<body>
|
|
566
|
+
<div class="container">
|
|
567
|
+
<div class="header">
|
|
568
|
+
<h1>New Curation Candidate</h1>
|
|
569
|
+
<p>Pattern ready for review</p>
|
|
570
|
+
</div>
|
|
571
|
+
<div class="content">
|
|
572
|
+
<h2>${patternName}</h2>
|
|
573
|
+
<p><strong>Category:</strong> ${category}</p>
|
|
574
|
+
|
|
575
|
+
${evidence ? `
|
|
576
|
+
<div class="metrics">
|
|
577
|
+
<div class="metric">
|
|
578
|
+
<div class="metric-value">${(evidence.correlation * 100).toFixed(0)}%</div>
|
|
579
|
+
<div class="metric-label">Success Rate</div>
|
|
580
|
+
</div>
|
|
581
|
+
<div class="metric">
|
|
582
|
+
<div class="metric-value">${evidence.projectCount || 0}</div>
|
|
583
|
+
<div class="metric-label">Projects</div>
|
|
584
|
+
</div>
|
|
585
|
+
<div class="metric">
|
|
586
|
+
<div class="metric-value">${evidence.developerCount || 0}</div>
|
|
587
|
+
<div class="metric-label">Developers</div>
|
|
588
|
+
</div>
|
|
589
|
+
<div class="metric">
|
|
590
|
+
<div class="metric-value">${evidence.sessionCount || 0}</div>
|
|
591
|
+
<div class="metric-label">Sessions</div>
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
` : ''}
|
|
595
|
+
|
|
596
|
+
<p>This pattern has met all promotion criteria and is ready for human review before being promoted to .equilateral-standards/</p>
|
|
597
|
+
|
|
598
|
+
<a href="${this.config.appUrl}/admin/curation/${candidateId}" class="button">Review Candidate</a>
|
|
599
|
+
<a href="${this.config.appUrl}/admin/curation" class="button button-secondary">View All Candidates</a>
|
|
600
|
+
</div>
|
|
601
|
+
<div class="footer">
|
|
602
|
+
<p>You received this because you're a standards curator.</p>
|
|
603
|
+
<p><a href="${this.config.appUrl}/settings/notifications">Manage notification preferences</a></p>
|
|
604
|
+
</div>
|
|
605
|
+
</div>
|
|
606
|
+
</body>
|
|
607
|
+
</html>`;
|
|
608
|
+
|
|
609
|
+
const text = `New Curation Candidate: ${patternName}
|
|
610
|
+
|
|
611
|
+
Category: ${category}
|
|
612
|
+
|
|
613
|
+
${evidence ? `Evidence:
|
|
614
|
+
- Success Rate: ${(evidence.correlation * 100).toFixed(0)}%
|
|
615
|
+
- Projects: ${evidence.projectCount || 0}
|
|
616
|
+
- Developers: ${evidence.developerCount || 0}
|
|
617
|
+
- Sessions: ${evidence.sessionCount || 0}` : ''}
|
|
618
|
+
|
|
619
|
+
This pattern has met all promotion criteria and is ready for human review.
|
|
620
|
+
|
|
621
|
+
Review candidate: ${this.config.appUrl}/admin/curation/${candidateId}
|
|
622
|
+
View all candidates: ${this.config.appUrl}/admin/curation
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
Manage notification preferences: ${this.config.appUrl}/settings/notifications`;
|
|
626
|
+
|
|
627
|
+
return { subject, html, text };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Build generic email
|
|
632
|
+
*/
|
|
633
|
+
buildGenericEmail(type, data) {
|
|
634
|
+
const subject = `MindMeld Notification: ${type}`;
|
|
635
|
+
|
|
636
|
+
const html = `
|
|
637
|
+
<!DOCTYPE html>
|
|
638
|
+
<html>
|
|
639
|
+
<head>
|
|
640
|
+
<style>
|
|
641
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
|
642
|
+
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
643
|
+
.header { background: #2563eb; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
|
|
644
|
+
.content { background: #f8fafc; padding: 20px; border-radius: 0 0 8px 8px; }
|
|
645
|
+
.footer { margin-top: 20px; font-size: 12px; color: #94a3b8; }
|
|
646
|
+
</style>
|
|
647
|
+
</head>
|
|
648
|
+
<body>
|
|
649
|
+
<div class="container">
|
|
650
|
+
<div class="header">
|
|
651
|
+
<h1>MindMeld Notification</h1>
|
|
652
|
+
</div>
|
|
653
|
+
<div class="content">
|
|
654
|
+
<p><strong>Type:</strong> ${type}</p>
|
|
655
|
+
<pre>${JSON.stringify(data, null, 2)}</pre>
|
|
656
|
+
</div>
|
|
657
|
+
<div class="footer">
|
|
658
|
+
<p><a href="${this.config.appUrl}/settings/notifications">Manage notification preferences</a></p>
|
|
659
|
+
</div>
|
|
660
|
+
</div>
|
|
661
|
+
</body>
|
|
662
|
+
</html>`;
|
|
663
|
+
|
|
664
|
+
const text = `MindMeld Notification
|
|
665
|
+
|
|
666
|
+
Type: ${type}
|
|
667
|
+
|
|
668
|
+
${JSON.stringify(data, null, 2)}
|
|
669
|
+
|
|
670
|
+
---
|
|
671
|
+
Manage notification preferences: ${this.config.appUrl}/settings/notifications`;
|
|
672
|
+
|
|
673
|
+
return { subject, html, text };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Send Slack notification via webhook
|
|
678
|
+
*/
|
|
679
|
+
async sendSlack(type, data) {
|
|
680
|
+
const isCritical = type === 'critical_violation' || data?.severity === 'critical';
|
|
681
|
+
const webhookUrl = isCritical ? this.config.slackCriticalWebhookUrl : this.config.slackWebhookUrl;
|
|
682
|
+
|
|
683
|
+
if (!webhookUrl) {
|
|
684
|
+
return { sent: false, reason: 'no_webhook_url' };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const payload = this.buildSlackPayload(type, data);
|
|
688
|
+
|
|
689
|
+
const response = await fetch(webhookUrl, {
|
|
690
|
+
method: 'POST',
|
|
691
|
+
headers: { 'Content-Type': 'application/json' },
|
|
692
|
+
body: JSON.stringify(payload)
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
if (!response.ok) {
|
|
696
|
+
throw new Error(`Slack webhook failed: ${response.status}`);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
console.log(`[NotificationService] Slack notification sent: ${type}`);
|
|
700
|
+
return { sent: true };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Build Slack message payload based on notification type
|
|
705
|
+
*/
|
|
706
|
+
buildSlackPayload(type, data) {
|
|
707
|
+
switch (type) {
|
|
708
|
+
case 'pattern_promotion':
|
|
709
|
+
return this.buildSlackPatternPromotion(data);
|
|
710
|
+
case 'critical_violation':
|
|
711
|
+
return this.buildSlackCriticalViolation(data);
|
|
712
|
+
case 'team_alert':
|
|
713
|
+
return this.buildSlackTeamAlert(data);
|
|
714
|
+
case 'curation_candidate':
|
|
715
|
+
return this.buildSlackCurationCandidate(data);
|
|
716
|
+
default:
|
|
717
|
+
return this.buildSlackGeneric(type, data);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Build Slack pattern promotion payload
|
|
723
|
+
*/
|
|
724
|
+
buildSlackPatternPromotion(data) {
|
|
725
|
+
const { patternName, projectName, newMaturity, evidence } = data;
|
|
726
|
+
|
|
727
|
+
return {
|
|
728
|
+
blocks: [
|
|
729
|
+
{
|
|
730
|
+
type: 'header',
|
|
731
|
+
text: {
|
|
732
|
+
type: 'plain_text',
|
|
733
|
+
text: 'Pattern Promoted',
|
|
734
|
+
emoji: true
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
type: 'section',
|
|
739
|
+
fields: [
|
|
740
|
+
{
|
|
741
|
+
type: 'mrkdwn',
|
|
742
|
+
text: `*Pattern:*\n${patternName}`
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
type: 'mrkdwn',
|
|
746
|
+
text: `*Project:*\n${projectName}`
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
type: 'mrkdwn',
|
|
750
|
+
text: `*New Status:*\n${newMaturity}`
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
type: 'mrkdwn',
|
|
754
|
+
text: `*Success Rate:*\n${evidence ? (evidence.correlation * 100).toFixed(0) + '%' : 'N/A'}`
|
|
755
|
+
}
|
|
756
|
+
]
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
type: 'actions',
|
|
760
|
+
elements: [
|
|
761
|
+
{
|
|
762
|
+
type: 'button',
|
|
763
|
+
text: {
|
|
764
|
+
type: 'plain_text',
|
|
765
|
+
text: 'View Patterns'
|
|
766
|
+
},
|
|
767
|
+
url: `${this.config.appUrl}/dashboard/patterns`
|
|
768
|
+
}
|
|
769
|
+
]
|
|
770
|
+
}
|
|
771
|
+
]
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Build Slack critical violation payload
|
|
777
|
+
*/
|
|
778
|
+
buildSlackCriticalViolation(data) {
|
|
779
|
+
const { violationType, projectName, standardName, details, filePath } = data;
|
|
780
|
+
|
|
781
|
+
return {
|
|
782
|
+
blocks: [
|
|
783
|
+
{
|
|
784
|
+
type: 'header',
|
|
785
|
+
text: {
|
|
786
|
+
type: 'plain_text',
|
|
787
|
+
text: 'CRITICAL: Standards Violation',
|
|
788
|
+
emoji: true
|
|
789
|
+
}
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
type: 'section',
|
|
793
|
+
text: {
|
|
794
|
+
type: 'mrkdwn',
|
|
795
|
+
text: `*${standardName}*\n${details}`
|
|
796
|
+
}
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
type: 'section',
|
|
800
|
+
fields: [
|
|
801
|
+
{
|
|
802
|
+
type: 'mrkdwn',
|
|
803
|
+
text: `*Project:*\n${projectName}`
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
type: 'mrkdwn',
|
|
807
|
+
text: `*Type:*\n${violationType}`
|
|
808
|
+
}
|
|
809
|
+
]
|
|
810
|
+
},
|
|
811
|
+
filePath ? {
|
|
812
|
+
type: 'context',
|
|
813
|
+
elements: [
|
|
814
|
+
{
|
|
815
|
+
type: 'mrkdwn',
|
|
816
|
+
text: `File: \`${filePath}\``
|
|
817
|
+
}
|
|
818
|
+
]
|
|
819
|
+
} : null,
|
|
820
|
+
{
|
|
821
|
+
type: 'actions',
|
|
822
|
+
elements: [
|
|
823
|
+
{
|
|
824
|
+
type: 'button',
|
|
825
|
+
text: {
|
|
826
|
+
type: 'plain_text',
|
|
827
|
+
text: 'View Violations'
|
|
828
|
+
},
|
|
829
|
+
style: 'danger',
|
|
830
|
+
url: `${this.config.appUrl}/dashboard/violations`
|
|
831
|
+
}
|
|
832
|
+
]
|
|
833
|
+
}
|
|
834
|
+
].filter(Boolean)
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Build Slack team alert payload
|
|
840
|
+
*/
|
|
841
|
+
buildSlackTeamAlert(data) {
|
|
842
|
+
const { alertType, severity, userName, message, details } = data;
|
|
843
|
+
|
|
844
|
+
const severityEmoji = {
|
|
845
|
+
concern: ':red_circle:',
|
|
846
|
+
warning: ':large_orange_circle:',
|
|
847
|
+
info: ':large_blue_circle:'
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
return {
|
|
851
|
+
blocks: [
|
|
852
|
+
{
|
|
853
|
+
type: 'header',
|
|
854
|
+
text: {
|
|
855
|
+
type: 'plain_text',
|
|
856
|
+
text: 'Team Alert',
|
|
857
|
+
emoji: true
|
|
858
|
+
}
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
type: 'section',
|
|
862
|
+
text: {
|
|
863
|
+
type: 'mrkdwn',
|
|
864
|
+
text: `${severityEmoji[severity] || ''} *${message}*`
|
|
865
|
+
}
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
type: 'section',
|
|
869
|
+
fields: [
|
|
870
|
+
{
|
|
871
|
+
type: 'mrkdwn',
|
|
872
|
+
text: `*Team Member:*\n${userName}`
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
type: 'mrkdwn',
|
|
876
|
+
text: `*Severity:*\n${severity}`
|
|
877
|
+
}
|
|
878
|
+
]
|
|
879
|
+
},
|
|
880
|
+
details ? {
|
|
881
|
+
type: 'context',
|
|
882
|
+
elements: Object.entries(details).map(([key, value]) => ({
|
|
883
|
+
type: 'mrkdwn',
|
|
884
|
+
text: `*${key}:* ${value}`
|
|
885
|
+
}))
|
|
886
|
+
} : null,
|
|
887
|
+
{
|
|
888
|
+
type: 'actions',
|
|
889
|
+
elements: [
|
|
890
|
+
{
|
|
891
|
+
type: 'button',
|
|
892
|
+
text: {
|
|
893
|
+
type: 'plain_text',
|
|
894
|
+
text: 'View Alerts'
|
|
895
|
+
},
|
|
896
|
+
url: `${this.config.appUrl}/dashboard/alerts`
|
|
897
|
+
}
|
|
898
|
+
]
|
|
899
|
+
}
|
|
900
|
+
].filter(Boolean)
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Build Slack curation candidate payload
|
|
906
|
+
*/
|
|
907
|
+
buildSlackCurationCandidate(data) {
|
|
908
|
+
const { candidateId, patternName, category, evidence } = data;
|
|
909
|
+
|
|
910
|
+
return {
|
|
911
|
+
blocks: [
|
|
912
|
+
{
|
|
913
|
+
type: 'header',
|
|
914
|
+
text: {
|
|
915
|
+
type: 'plain_text',
|
|
916
|
+
text: 'New Curation Candidate',
|
|
917
|
+
emoji: true
|
|
918
|
+
}
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
type: 'section',
|
|
922
|
+
text: {
|
|
923
|
+
type: 'mrkdwn',
|
|
924
|
+
text: `*${patternName}*\nReady for review and promotion to .equilateral-standards/`
|
|
925
|
+
}
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
type: 'section',
|
|
929
|
+
fields: [
|
|
930
|
+
{
|
|
931
|
+
type: 'mrkdwn',
|
|
932
|
+
text: `*Category:*\n${category}`
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
type: 'mrkdwn',
|
|
936
|
+
text: `*Success Rate:*\n${evidence ? (evidence.correlation * 100).toFixed(0) + '%' : 'N/A'}`
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
type: 'mrkdwn',
|
|
940
|
+
text: `*Projects:*\n${evidence?.projectCount || 0}`
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
type: 'mrkdwn',
|
|
944
|
+
text: `*Developers:*\n${evidence?.developerCount || 0}`
|
|
945
|
+
}
|
|
946
|
+
]
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
type: 'actions',
|
|
950
|
+
elements: [
|
|
951
|
+
{
|
|
952
|
+
type: 'button',
|
|
953
|
+
text: {
|
|
954
|
+
type: 'plain_text',
|
|
955
|
+
text: 'Review Candidate'
|
|
956
|
+
},
|
|
957
|
+
style: 'primary',
|
|
958
|
+
url: `${this.config.appUrl}/admin/curation/${candidateId}`
|
|
959
|
+
}
|
|
960
|
+
]
|
|
961
|
+
}
|
|
962
|
+
]
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Build generic Slack payload
|
|
968
|
+
*/
|
|
969
|
+
buildSlackGeneric(type, data) {
|
|
970
|
+
return {
|
|
971
|
+
blocks: [
|
|
972
|
+
{
|
|
973
|
+
type: 'header',
|
|
974
|
+
text: {
|
|
975
|
+
type: 'plain_text',
|
|
976
|
+
text: `MindMeld: ${type}`,
|
|
977
|
+
emoji: true
|
|
978
|
+
}
|
|
979
|
+
},
|
|
980
|
+
{
|
|
981
|
+
type: 'section',
|
|
982
|
+
text: {
|
|
983
|
+
type: 'mrkdwn',
|
|
984
|
+
text: '```' + JSON.stringify(data, null, 2) + '```'
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
]
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Send batch notifications (for weekly digests, etc.)
|
|
993
|
+
*
|
|
994
|
+
* @param {Array} recipients - Array of { email, preferences, data }
|
|
995
|
+
* @param {string} type - Notification type
|
|
996
|
+
* @returns {Promise<Object>} Batch results
|
|
997
|
+
*/
|
|
998
|
+
async sendBatch(recipients, type) {
|
|
999
|
+
const results = {
|
|
1000
|
+
total: recipients.length,
|
|
1001
|
+
sent: 0,
|
|
1002
|
+
failed: 0,
|
|
1003
|
+
skipped: 0,
|
|
1004
|
+
errors: []
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
for (const recipient of recipients) {
|
|
1008
|
+
try {
|
|
1009
|
+
const result = await this.sendNotification({
|
|
1010
|
+
type,
|
|
1011
|
+
email: recipient.email,
|
|
1012
|
+
preferences: recipient.preferences,
|
|
1013
|
+
data: recipient.data,
|
|
1014
|
+
projectId: recipient.projectId
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
if (result.email?.sent || result.slack?.sent) {
|
|
1018
|
+
results.sent++;
|
|
1019
|
+
} else {
|
|
1020
|
+
results.skipped++;
|
|
1021
|
+
}
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
results.failed++;
|
|
1024
|
+
results.errors.push({ email: recipient.email, error: error.message });
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
return results;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
module.exports = { NotificationService };
|