@littlebearapps/platform-admin-sdk 1.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.
Files changed (94) hide show
  1. package/README.md +112 -0
  2. package/dist/index.d.ts +16 -0
  3. package/dist/index.js +89 -0
  4. package/dist/prompts.d.ts +27 -0
  5. package/dist/prompts.js +80 -0
  6. package/dist/scaffold.d.ts +5 -0
  7. package/dist/scaffold.js +65 -0
  8. package/dist/templates.d.ts +16 -0
  9. package/dist/templates.js +131 -0
  10. package/package.json +46 -0
  11. package/templates/full/migrations/006_pattern_discovery.sql +199 -0
  12. package/templates/full/migrations/007_notifications_search.sql +127 -0
  13. package/templates/full/workers/lib/pattern-discovery/ai-prompt.ts +644 -0
  14. package/templates/full/workers/lib/pattern-discovery/clustering.ts +278 -0
  15. package/templates/full/workers/lib/pattern-discovery/shadow-evaluation.ts +603 -0
  16. package/templates/full/workers/lib/pattern-discovery/storage.ts +806 -0
  17. package/templates/full/workers/lib/pattern-discovery/types.ts +159 -0
  18. package/templates/full/workers/lib/pattern-discovery/validation.ts +278 -0
  19. package/templates/full/workers/pattern-discovery.ts +661 -0
  20. package/templates/full/workers/platform-alert-router.ts +1809 -0
  21. package/templates/full/workers/platform-notifications.ts +424 -0
  22. package/templates/full/workers/platform-search.ts +480 -0
  23. package/templates/full/workers/platform-settings.ts +436 -0
  24. package/templates/full/wrangler.alert-router.jsonc.hbs +34 -0
  25. package/templates/full/wrangler.notifications.jsonc.hbs +23 -0
  26. package/templates/full/wrangler.pattern-discovery.jsonc.hbs +33 -0
  27. package/templates/full/wrangler.search.jsonc.hbs +16 -0
  28. package/templates/full/wrangler.settings.jsonc.hbs +23 -0
  29. package/templates/shared/README.md.hbs +69 -0
  30. package/templates/shared/config/budgets.yaml.hbs +72 -0
  31. package/templates/shared/config/services.yaml.hbs +45 -0
  32. package/templates/shared/migrations/001_core_tables.sql +117 -0
  33. package/templates/shared/migrations/002_usage_warehouse.sql +830 -0
  34. package/templates/shared/migrations/003_feature_tracking.sql +250 -0
  35. package/templates/shared/migrations/004_settings_alerts.sql +452 -0
  36. package/templates/shared/migrations/seed.sql.hbs +4 -0
  37. package/templates/shared/package.json.hbs +21 -0
  38. package/templates/shared/scripts/sync-config.ts +242 -0
  39. package/templates/shared/tsconfig.json +12 -0
  40. package/templates/shared/workers/lib/analytics-engine.ts +357 -0
  41. package/templates/shared/workers/lib/billing.ts +293 -0
  42. package/templates/shared/workers/lib/circuit-breaker-middleware.ts +25 -0
  43. package/templates/shared/workers/lib/control.ts +292 -0
  44. package/templates/shared/workers/lib/economics.ts +368 -0
  45. package/templates/shared/workers/lib/metrics.ts +103 -0
  46. package/templates/shared/workers/lib/platform-settings.ts +407 -0
  47. package/templates/shared/workers/lib/shared/allowances.ts +333 -0
  48. package/templates/shared/workers/lib/shared/cloudflare.ts +1362 -0
  49. package/templates/shared/workers/lib/shared/types.ts +58 -0
  50. package/templates/shared/workers/lib/telemetry-sampling.ts +360 -0
  51. package/templates/shared/workers/lib/usage/collectors/example.ts +96 -0
  52. package/templates/shared/workers/lib/usage/collectors/index.ts +128 -0
  53. package/templates/shared/workers/lib/usage/handlers/audit.ts +306 -0
  54. package/templates/shared/workers/lib/usage/handlers/backfill.ts +845 -0
  55. package/templates/shared/workers/lib/usage/handlers/behavioral.ts +429 -0
  56. package/templates/shared/workers/lib/usage/handlers/data-queries.ts +507 -0
  57. package/templates/shared/workers/lib/usage/handlers/dlq-admin.ts +364 -0
  58. package/templates/shared/workers/lib/usage/handlers/health-trends.ts +222 -0
  59. package/templates/shared/workers/lib/usage/handlers/index.ts +35 -0
  60. package/templates/shared/workers/lib/usage/handlers/usage-admin.ts +421 -0
  61. package/templates/shared/workers/lib/usage/handlers/usage-features.ts +1262 -0
  62. package/templates/shared/workers/lib/usage/handlers/usage-metrics.ts +2420 -0
  63. package/templates/shared/workers/lib/usage/handlers/usage-settings.ts +610 -0
  64. package/templates/shared/workers/lib/usage/queue/budget-enforcement.ts +1032 -0
  65. package/templates/shared/workers/lib/usage/queue/cost-budget-enforcement.ts +128 -0
  66. package/templates/shared/workers/lib/usage/queue/cost-calculator.ts +77 -0
  67. package/templates/shared/workers/lib/usage/queue/dlq-handler.ts +161 -0
  68. package/templates/shared/workers/lib/usage/queue/index.ts +19 -0
  69. package/templates/shared/workers/lib/usage/queue/telemetry-processor.ts +790 -0
  70. package/templates/shared/workers/lib/usage/scheduled/anomaly-detection.ts +732 -0
  71. package/templates/shared/workers/lib/usage/scheduled/data-collection.ts +956 -0
  72. package/templates/shared/workers/lib/usage/scheduled/error-digest.ts +343 -0
  73. package/templates/shared/workers/lib/usage/scheduled/index.ts +18 -0
  74. package/templates/shared/workers/lib/usage/scheduled/rollups.ts +1561 -0
  75. package/templates/shared/workers/lib/usage/shared/constants.ts +362 -0
  76. package/templates/shared/workers/lib/usage/shared/index.ts +14 -0
  77. package/templates/shared/workers/lib/usage/shared/types.ts +1066 -0
  78. package/templates/shared/workers/lib/usage/shared/utils.ts +795 -0
  79. package/templates/shared/workers/platform-usage.ts +1915 -0
  80. package/templates/shared/wrangler.usage.jsonc.hbs +58 -0
  81. package/templates/standard/migrations/005_error_collection.sql +162 -0
  82. package/templates/standard/workers/error-collector.ts +2670 -0
  83. package/templates/standard/workers/lib/error-collector/capture.ts +213 -0
  84. package/templates/standard/workers/lib/error-collector/digest.ts +448 -0
  85. package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +262 -0
  86. package/templates/standard/workers/lib/error-collector/fingerprint.ts +258 -0
  87. package/templates/standard/workers/lib/error-collector/gap-alerts.ts +293 -0
  88. package/templates/standard/workers/lib/error-collector/github.ts +329 -0
  89. package/templates/standard/workers/lib/error-collector/types.ts +262 -0
  90. package/templates/standard/workers/lib/sentinel/gap-detection.ts +734 -0
  91. package/templates/standard/workers/lib/shared/slack-alerts.ts +585 -0
  92. package/templates/standard/workers/platform-sentinel.ts +1744 -0
  93. package/templates/standard/wrangler.error-collector.jsonc.hbs +44 -0
  94. package/templates/standard/wrangler.sentinel.jsonc.hbs +45 -0
@@ -0,0 +1,585 @@
1
+ /**
2
+ * Unified Slack Alerts Module
3
+ *
4
+ * Provides a single interface for:
5
+ * - Sending Slack alerts with consistent formatting
6
+ * - Creating in-app notifications (D1)
7
+ * - KV-based deduplication (1-hour window)
8
+ *
9
+ * Usage:
10
+ * ```ts
11
+ * import { sendAlert, sendAlertWithNotification } from './lib/shared/slack-alerts';
12
+ *
13
+ * // Just Slack
14
+ * await sendAlert(env, {
15
+ * source: 'circuit-breaker',
16
+ * priority: 'critical',
17
+ * title: 'Circuit Breaker Tripped',
18
+ * message: 'Feature brand-copilot:scanner exceeded budget',
19
+ * project: 'brand-copilot',
20
+ * });
21
+ *
22
+ * // Slack + In-app notification
23
+ * await sendAlertWithNotification(env, {
24
+ * source: 'error-collector',
25
+ * priority: 'high',
26
+ * title: 'New P1 Error Detected',
27
+ * message: 'TypeError in my-project:worker',
28
+ * project: 'my-project',
29
+ * actionUrl: '/errors',
30
+ * actionLabel: 'View Errors',
31
+ * });
32
+ * ```
33
+ *
34
+ * @module workers/lib/shared/slack-alerts
35
+ */
36
+
37
+ import type { KVNamespace, D1Database } from '@cloudflare/workers-types';
38
+
39
+ // TODO: Set your dashboard URL
40
+ const DASHBOARD_URL = 'https://your-dashboard.example.com';
41
+
42
+ // =============================================================================
43
+ // TYPES
44
+ // =============================================================================
45
+
46
+ export type AlertSource =
47
+ | 'error-collector'
48
+ | 'pattern-discovery'
49
+ | 'circuit-breaker'
50
+ | 'usage'
51
+ | 'gap-detection'
52
+ | 'gatus'
53
+ | 'system'
54
+ | 'sentinel';
55
+
56
+ export type AlertPriority = 'critical' | 'high' | 'medium' | 'low' | 'info';
57
+
58
+ export type NotificationCategory = 'error' | 'warning' | 'info' | 'success';
59
+
60
+ export interface SlackAlert {
61
+ source: AlertSource;
62
+ priority: AlertPriority;
63
+ title: string;
64
+ message: string;
65
+ project?: string;
66
+ context?: Record<string, string | number | boolean>;
67
+ actionUrl?: string;
68
+ actionLabel?: string;
69
+ /** Additional Slack blocks to include */
70
+ additionalBlocks?: Array<Record<string, unknown>>;
71
+ /** Skip deduplication for this alert */
72
+ skipDedup?: boolean;
73
+ }
74
+
75
+ export interface AlertEnv {
76
+ SLACK_WEBHOOK_URL?: string;
77
+ PLATFORM_CACHE?: KVNamespace;
78
+ PLATFORM_DB?: D1Database;
79
+ }
80
+
81
+ // =============================================================================
82
+ // CONSTANTS
83
+ // =============================================================================
84
+
85
+ const DEDUPE_TTL = 3600; // 1 hour in seconds
86
+ const DEDUPE_PREFIX = 'SLACK_ALERT:';
87
+
88
+ /** Priority to Slack colour mapping */
89
+ const PRIORITY_COLOURS: Record<AlertPriority, string> = {
90
+ critical: '#d32f2f', // Red
91
+ high: '#ff9800', // Orange
92
+ medium: '#ffc107', // Yellow
93
+ low: '#2196f3', // Blue
94
+ info: '#36a64f', // Green
95
+ };
96
+
97
+ /** Priority to emoji mapping */
98
+ const PRIORITY_EMOJI: Record<AlertPriority, string> = {
99
+ critical: '🚨',
100
+ high: '⚠️',
101
+ medium: '📢',
102
+ low: 'ℹ️',
103
+ info: '✅',
104
+ };
105
+
106
+ /** Source to human-readable label */
107
+ const SOURCE_LABELS: Record<AlertSource, string> = {
108
+ 'error-collector': 'Error Collector',
109
+ 'pattern-discovery': 'Pattern Discovery',
110
+ 'circuit-breaker': 'Circuit Breaker',
111
+ usage: 'Usage Monitor',
112
+ 'gap-detection': 'Gap Detection',
113
+ gatus: 'Gatus',
114
+ system: 'System',
115
+ sentinel: 'Platform Sentinel',
116
+ };
117
+
118
+ /** Map priority to notification category */
119
+ const PRIORITY_TO_CATEGORY: Record<AlertPriority, NotificationCategory> = {
120
+ critical: 'error',
121
+ high: 'error',
122
+ medium: 'warning',
123
+ low: 'info',
124
+ info: 'success',
125
+ };
126
+
127
+ // =============================================================================
128
+ // DEDUPLICATION
129
+ // =============================================================================
130
+
131
+ /**
132
+ * Generate a deduplication key from alert content
133
+ */
134
+ function generateDedupeKey(alert: SlackAlert): string {
135
+ const content = `${alert.source}:${alert.priority}:${alert.title}:${alert.project || 'all'}`;
136
+ // Simple hash for deduplication
137
+ let hash = 0;
138
+ for (let i = 0; i < content.length; i++) {
139
+ const char = content.charCodeAt(i);
140
+ hash = (hash << 5) - hash + char;
141
+ hash = hash & hash; // Convert to 32-bit integer
142
+ }
143
+ return `${DEDUPE_PREFIX}${Math.abs(hash).toString(36)}`;
144
+ }
145
+
146
+ /**
147
+ * Check if alert was recently sent (within dedupe window)
148
+ */
149
+ async function isDuplicate(kv: KVNamespace | undefined, alert: SlackAlert): Promise<boolean> {
150
+ if (!kv || alert.skipDedup) return false;
151
+
152
+ const key = generateDedupeKey(alert);
153
+ const existing = await kv.get(key);
154
+ return existing !== null;
155
+ }
156
+
157
+ /**
158
+ * Mark alert as sent for deduplication
159
+ */
160
+ async function markSent(kv: KVNamespace | undefined, alert: SlackAlert): Promise<void> {
161
+ if (!kv || alert.skipDedup) return;
162
+
163
+ const key = generateDedupeKey(alert);
164
+ await kv.put(key, new Date().toISOString(), { expirationTtl: DEDUPE_TTL });
165
+ }
166
+
167
+ // =============================================================================
168
+ // SLACK MESSAGE BUILDING
169
+ // =============================================================================
170
+
171
+ /**
172
+ * Build Slack message payload
173
+ */
174
+ function buildSlackMessage(alert: SlackAlert): Record<string, unknown> {
175
+ const emoji = PRIORITY_EMOJI[alert.priority];
176
+ const colour = PRIORITY_COLOURS[alert.priority];
177
+ const sourceLabel = SOURCE_LABELS[alert.source] || alert.source;
178
+ const priorityLabel = alert.priority.toUpperCase();
179
+
180
+ // Build header
181
+ const headerText = `${emoji} [${priorityLabel}] ${alert.title}`;
182
+
183
+ // Build context fields
184
+ const contextFields: Array<{ type: string; text: string }> = [];
185
+
186
+ if (alert.project) {
187
+ contextFields.push({
188
+ type: 'mrkdwn',
189
+ text: `*Project:* ${alert.project}`,
190
+ });
191
+ }
192
+
193
+ contextFields.push({
194
+ type: 'mrkdwn',
195
+ text: `*Source:* ${sourceLabel}`,
196
+ });
197
+
198
+ contextFields.push({
199
+ type: 'mrkdwn',
200
+ text: `*Time:* ${new Date().toISOString()}`,
201
+ });
202
+
203
+ // Build additional context if provided
204
+ if (alert.context) {
205
+ for (const [key, value] of Object.entries(alert.context)) {
206
+ contextFields.push({
207
+ type: 'mrkdwn',
208
+ text: `*${key}:* ${value}`,
209
+ });
210
+ }
211
+ }
212
+
213
+ // Build blocks
214
+ const blocks: Array<Record<string, unknown>> = [
215
+ {
216
+ type: 'header',
217
+ text: {
218
+ type: 'plain_text',
219
+ text: headerText,
220
+ emoji: true,
221
+ },
222
+ },
223
+ {
224
+ type: 'section',
225
+ text: {
226
+ type: 'mrkdwn',
227
+ text: alert.message,
228
+ },
229
+ },
230
+ {
231
+ type: 'section',
232
+ fields: contextFields.slice(0, 10), // Slack limit
233
+ },
234
+ ];
235
+
236
+ // Add action buttons if URL provided
237
+ if (alert.actionUrl) {
238
+ blocks.push({
239
+ type: 'actions',
240
+ elements: [
241
+ {
242
+ type: 'button',
243
+ text: {
244
+ type: 'plain_text',
245
+ text: alert.actionLabel || 'View Details',
246
+ emoji: true,
247
+ },
248
+ url: alert.actionUrl,
249
+ action_id: 'view_details',
250
+ },
251
+ {
252
+ type: 'button',
253
+ text: {
254
+ type: 'plain_text',
255
+ text: '📊 Dashboard',
256
+ emoji: true,
257
+ },
258
+ url: `${DASHBOARD_URL}/dashboard`,
259
+ action_id: 'open_dashboard',
260
+ },
261
+ ],
262
+ });
263
+ }
264
+
265
+ // Add any additional blocks
266
+ if (alert.additionalBlocks) {
267
+ blocks.push(...alert.additionalBlocks);
268
+ }
269
+
270
+ return {
271
+ text: headerText, // Fallback for notifications
272
+ blocks,
273
+ attachments: [
274
+ {
275
+ color: colour,
276
+ footer: `Platform Alerts | ${sourceLabel}`,
277
+ ts: Math.floor(Date.now() / 1000),
278
+ },
279
+ ],
280
+ };
281
+ }
282
+
283
+ // =============================================================================
284
+ // NOTIFICATION CREATION
285
+ // =============================================================================
286
+
287
+ /**
288
+ * Generate notification ID
289
+ */
290
+ function generateNotificationId(): string {
291
+ return `notif_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
292
+ }
293
+
294
+ /**
295
+ * Create in-app notification in D1
296
+ */
297
+ async function createNotification(
298
+ db: D1Database | undefined,
299
+ alert: SlackAlert
300
+ ): Promise<string | null> {
301
+ if (!db) return null;
302
+
303
+ const id = generateNotificationId();
304
+ const now = Math.floor(Date.now() / 1000);
305
+ const category = PRIORITY_TO_CATEGORY[alert.priority];
306
+
307
+ try {
308
+ await db
309
+ .prepare(
310
+ `INSERT INTO notifications (id, category, source, source_id, title, description, priority, action_url, action_label, project, created_at, expires_at)
311
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
312
+ )
313
+ .bind(
314
+ id,
315
+ category,
316
+ alert.source,
317
+ null, // source_id - could be extended for linking
318
+ alert.title,
319
+ alert.message,
320
+ alert.priority,
321
+ alert.actionUrl || null,
322
+ alert.actionLabel || null,
323
+ alert.project || null,
324
+ now,
325
+ null // no expiry by default
326
+ )
327
+ .run();
328
+
329
+ return id;
330
+ } catch (error) {
331
+ console.error('[slack-alerts] Failed to create notification:', error);
332
+ return null;
333
+ }
334
+ }
335
+
336
+ // =============================================================================
337
+ // PUBLIC API
338
+ // =============================================================================
339
+
340
+ /**
341
+ * Send Slack alert only (no in-app notification)
342
+ *
343
+ * @param env Environment with SLACK_WEBHOOK_URL and optionally PLATFORM_CACHE for dedup
344
+ * @param alert Alert details
345
+ * @returns true if sent, false if deduplicated or failed
346
+ */
347
+ export async function sendAlert(env: AlertEnv, alert: SlackAlert): Promise<boolean> {
348
+ // Check deduplication
349
+ if (await isDuplicate(env.PLATFORM_CACHE, alert)) {
350
+ console.log('[slack-alerts] Alert deduplicated:', alert.title);
351
+ return false;
352
+ }
353
+
354
+ // Check webhook URL
355
+ if (!env.SLACK_WEBHOOK_URL) {
356
+ console.warn('[slack-alerts] SLACK_WEBHOOK_URL not configured, skipping Slack');
357
+ return false;
358
+ }
359
+
360
+ // Build and send message
361
+ const message = buildSlackMessage(alert);
362
+
363
+ try {
364
+ const response = await fetch(env.SLACK_WEBHOOK_URL, {
365
+ method: 'POST',
366
+ headers: { 'Content-Type': 'application/json' },
367
+ body: JSON.stringify(message),
368
+ });
369
+
370
+ if (!response.ok) {
371
+ console.error('[slack-alerts] Slack webhook failed:', response.status);
372
+ return false;
373
+ }
374
+
375
+ // Mark as sent for deduplication
376
+ await markSent(env.PLATFORM_CACHE, alert);
377
+
378
+ return true;
379
+ } catch (error) {
380
+ console.error('[slack-alerts] Failed to send Slack alert:', error);
381
+ return false;
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Send Slack alert AND create in-app notification
387
+ *
388
+ * Creates notification FIRST (so it's available even if Slack fails),
389
+ * then sends Slack alert.
390
+ *
391
+ * @param env Environment with SLACK_WEBHOOK_URL, PLATFORM_CACHE, PLATFORM_DB
392
+ * @param alert Alert details
393
+ * @returns Object with notification ID and Slack success status
394
+ */
395
+ export async function sendAlertWithNotification(
396
+ env: AlertEnv,
397
+ alert: SlackAlert
398
+ ): Promise<{ notificationId: string | null; slackSent: boolean }> {
399
+ // Check deduplication first
400
+ if (await isDuplicate(env.PLATFORM_CACHE, alert)) {
401
+ console.log('[slack-alerts] Alert deduplicated:', alert.title);
402
+ return { notificationId: null, slackSent: false };
403
+ }
404
+
405
+ // Create notification first (more reliable than Slack)
406
+ const notificationId = await createNotification(env.PLATFORM_DB, alert);
407
+
408
+ // Then try Slack (graceful degradation)
409
+ let slackSent = false;
410
+
411
+ if (env.SLACK_WEBHOOK_URL) {
412
+ const message = buildSlackMessage(alert);
413
+
414
+ try {
415
+ const response = await fetch(env.SLACK_WEBHOOK_URL, {
416
+ method: 'POST',
417
+ headers: { 'Content-Type': 'application/json' },
418
+ body: JSON.stringify(message),
419
+ });
420
+
421
+ slackSent = response.ok;
422
+
423
+ if (!response.ok) {
424
+ console.error('[slack-alerts] Slack webhook failed:', response.status);
425
+ }
426
+ } catch (error) {
427
+ console.error('[slack-alerts] Failed to send Slack alert:', error);
428
+ }
429
+ }
430
+
431
+ // Mark as sent for deduplication (even if just notification was created)
432
+ if (notificationId || slackSent) {
433
+ await markSent(env.PLATFORM_CACHE, alert);
434
+ }
435
+
436
+ return { notificationId, slackSent };
437
+ }
438
+
439
+ /**
440
+ * Create notification only (no Slack)
441
+ *
442
+ * Useful for lower-priority alerts that don't need Slack.
443
+ *
444
+ * @param env Environment with PLATFORM_DB
445
+ * @param alert Alert details
446
+ * @returns Notification ID or null if failed
447
+ */
448
+ export async function createNotificationOnly(
449
+ env: AlertEnv,
450
+ alert: SlackAlert
451
+ ): Promise<string | null> {
452
+ return createNotification(env.PLATFORM_DB, alert);
453
+ }
454
+
455
+ // =============================================================================
456
+ // CONVENIENCE FUNCTIONS
457
+ // =============================================================================
458
+
459
+ /**
460
+ * Send a critical circuit breaker alert
461
+ */
462
+ export async function sendCircuitBreakerAlert(
463
+ env: AlertEnv,
464
+ options: {
465
+ featureKey: string;
466
+ project: string;
467
+ budgetLimit: number;
468
+ currentUsage: number;
469
+ reason: 'tripped' | 'warning' | 'recovered';
470
+ }
471
+ ): Promise<{ notificationId: string | null; slackSent: boolean }> {
472
+ const { featureKey, project, budgetLimit, currentUsage, reason } = options;
473
+
474
+ const priority: AlertPriority = reason === 'tripped' ? 'critical' : reason === 'warning' ? 'high' : 'info';
475
+ const title =
476
+ reason === 'tripped'
477
+ ? `Circuit Breaker Tripped: ${featureKey}`
478
+ : reason === 'warning'
479
+ ? `Budget Warning (80%): ${featureKey}`
480
+ : `Circuit Breaker Recovered: ${featureKey}`;
481
+
482
+ const message =
483
+ reason === 'recovered'
484
+ ? `Feature \`${featureKey}\` has recovered and is now operational.`
485
+ : `Feature \`${featureKey}\` is at ${((currentUsage / budgetLimit) * 100).toFixed(1)}% of budget.\n\n*Budget:* ${budgetLimit}\n*Current Usage:* ${currentUsage}`;
486
+
487
+ return sendAlertWithNotification(env, {
488
+ source: 'circuit-breaker',
489
+ priority,
490
+ title,
491
+ message,
492
+ project,
493
+ context: {
494
+ 'Feature Key': featureKey,
495
+ 'Budget Limit': budgetLimit,
496
+ 'Current Usage': currentUsage,
497
+ 'Usage %': `${((currentUsage / budgetLimit) * 100).toFixed(1)}%`,
498
+ },
499
+ actionUrl: `${DASHBOARD_URL}/circuit-breakers`,
500
+ actionLabel: 'View Circuit Breakers',
501
+ });
502
+ }
503
+
504
+ /**
505
+ * Send a gap detection alert
506
+ */
507
+ export async function sendGapAlert(
508
+ env: AlertEnv,
509
+ options: {
510
+ project: string;
511
+ coveragePercent: number;
512
+ missingResources: string[];
513
+ repoPath?: string;
514
+ }
515
+ ): Promise<{ notificationId: string | null; slackSent: boolean }> {
516
+ const { project, coveragePercent, missingResources, repoPath } = options;
517
+
518
+ const title = `Coverage Gap Detected: ${project}`;
519
+ const message =
520
+ `Project \`${project}\` has ${coveragePercent.toFixed(1)}% coverage (target: 90%).\n\n` +
521
+ `*Missing Resources:*\n${missingResources.slice(0, 5).map((r) => `• ${r}`).join('\n')}` +
522
+ (missingResources.length > 5 ? `\n_...and ${missingResources.length - 5} more_` : '');
523
+
524
+ return sendAlertWithNotification(env, {
525
+ source: 'gap-detection',
526
+ priority: 'medium',
527
+ title,
528
+ message,
529
+ project,
530
+ context: {
531
+ Coverage: `${coveragePercent.toFixed(1)}%`,
532
+ 'Missing Count': missingResources.length,
533
+ ...(repoPath ? { Repository: repoPath } : {}),
534
+ },
535
+ actionUrl: `${DASHBOARD_URL}/reports/gap-detection`,
536
+ actionLabel: 'View Gap Report',
537
+ });
538
+ }
539
+
540
+ /**
541
+ * Send a cost threshold alert
542
+ */
543
+ export async function sendCostAlert(
544
+ env: AlertEnv,
545
+ options: {
546
+ project: string;
547
+ currentCost: number;
548
+ threshold: number;
549
+ period: 'daily' | 'weekly' | 'monthly';
550
+ costBreakdown?: Record<string, number>;
551
+ }
552
+ ): Promise<{ notificationId: string | null; slackSent: boolean }> {
553
+ const { project, currentCost, threshold, period, costBreakdown } = options;
554
+
555
+ const priority: AlertPriority = currentCost > threshold * 1.5 ? 'critical' : 'high';
556
+ const title = `Cost ${priority === 'critical' ? 'Spike' : 'Warning'}: ${project}`;
557
+
558
+ let message = `${period.charAt(0).toUpperCase() + period.slice(1)} cost for \`${project}\` is $${currentCost.toFixed(2)} (threshold: $${threshold.toFixed(2)}).`;
559
+
560
+ if (costBreakdown && Object.keys(costBreakdown).length > 0) {
561
+ message +=
562
+ '\n\n*Cost Breakdown:*\n' +
563
+ Object.entries(costBreakdown)
564
+ .sort(([, a], [, b]) => b - a)
565
+ .slice(0, 5)
566
+ .map(([resource, cost]) => `• ${resource}: $${cost.toFixed(2)}`)
567
+ .join('\n');
568
+ }
569
+
570
+ return sendAlertWithNotification(env, {
571
+ source: 'sentinel',
572
+ priority,
573
+ title,
574
+ message,
575
+ project,
576
+ context: {
577
+ 'Current Cost': `$${currentCost.toFixed(2)}`,
578
+ Threshold: `$${threshold.toFixed(2)}`,
579
+ Period: period,
580
+ 'Over By': `${(((currentCost - threshold) / threshold) * 100).toFixed(1)}%`,
581
+ },
582
+ actionUrl: `${DASHBOARD_URL}/costs`,
583
+ actionLabel: 'View Costs',
584
+ });
585
+ }