waengine 1.7.3 → 1.7.4

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.
@@ -0,0 +1,867 @@
1
+ import { getStorage } from "./storage.js";
2
+ import { ErrorHandler } from "./error-handler.js";
3
+
4
+ export class ReportingManager {
5
+ constructor(client) {
6
+ this.client = client;
7
+ this.storage = getStorage();
8
+ this.errorHandler = new ErrorHandler();
9
+ this.reports = new Map();
10
+ this.scheduledReports = new Map();
11
+ this.dashboards = new Map();
12
+ this.metrics = new Map();
13
+ this.alerts = new Map();
14
+
15
+ this.initializeReporting();
16
+ }
17
+
18
+ // ===== INITIALIZATION =====
19
+
20
+ initializeReporting() {
21
+ this.loadReportingData();
22
+ this.setupDefaultMetrics();
23
+ this.startReportingJobs();
24
+ }
25
+
26
+ loadReportingData() {
27
+ try {
28
+ const reportingData = this.storage.read.from("reporting").get("data") || {};
29
+ this.reports = new Map(Object.entries(reportingData.reports || {}));
30
+ this.scheduledReports = new Map(Object.entries(reportingData.scheduledReports || {}));
31
+ this.dashboards = new Map(Object.entries(reportingData.dashboards || {}));
32
+ this.metrics = new Map(Object.entries(reportingData.metrics || {}));
33
+ this.alerts = new Map(Object.entries(reportingData.alerts || {}));
34
+ } catch (error) {
35
+ this.errorHandler.handle(error, 'ReportingManager.loadReportingData');
36
+ }
37
+ }
38
+
39
+ setupDefaultMetrics() {
40
+ // Message metrics
41
+ this.registerMetric('messages_sent', {
42
+ name: 'Messages Sent',
43
+ description: 'Total number of messages sent',
44
+ type: 'counter',
45
+ aggregation: 'sum',
46
+ category: 'messaging'
47
+ });
48
+
49
+ this.registerMetric('messages_received', {
50
+ name: 'Messages Received',
51
+ description: 'Total number of messages received',
52
+ type: 'counter',
53
+ aggregation: 'sum',
54
+ category: 'messaging'
55
+ });
56
+
57
+ // User metrics
58
+ this.registerMetric('active_users', {
59
+ name: 'Active Users',
60
+ description: 'Number of active users',
61
+ type: 'gauge',
62
+ aggregation: 'count_distinct',
63
+ category: 'users'
64
+ });
65
+
66
+ this.registerMetric('new_users', {
67
+ name: 'New Users',
68
+ description: 'Number of new users',
69
+ type: 'counter',
70
+ aggregation: 'sum',
71
+ category: 'users'
72
+ });
73
+
74
+ // Performance metrics
75
+ this.registerMetric('response_time', {
76
+ name: 'Response Time',
77
+ description: 'Average response time in milliseconds',
78
+ type: 'histogram',
79
+ aggregation: 'avg',
80
+ category: 'performance'
81
+ });
82
+
83
+ this.registerMetric('error_rate', {
84
+ name: 'Error Rate',
85
+ description: 'Percentage of errors',
86
+ type: 'gauge',
87
+ aggregation: 'rate',
88
+ category: 'performance'
89
+ });
90
+ }
91
+
92
+ startReportingJobs() {
93
+ // Generate scheduled reports
94
+ setInterval(() => {
95
+ this.processScheduledReports();
96
+ }, 60000); // Every minute
97
+
98
+ // Update real-time metrics
99
+ setInterval(() => {
100
+ this.updateRealTimeMetrics();
101
+ }, 30000); // Every 30 seconds
102
+
103
+ // Check alerts
104
+ setInterval(() => {
105
+ this.checkAlerts();
106
+ }, 60000); // Every minute
107
+ }
108
+
109
+ // ===== METRIC MANAGEMENT =====
110
+
111
+ registerMetric(metricId, config) {
112
+ const metric = {
113
+ id: metricId,
114
+ name: config.name,
115
+ description: config.description,
116
+ type: config.type, // counter, gauge, histogram
117
+ aggregation: config.aggregation, // sum, avg, count, count_distinct, rate
118
+ category: config.category,
119
+ unit: config.unit || '',
120
+ tags: config.tags || [],
121
+ createdAt: new Date(),
122
+ isActive: true
123
+ };
124
+
125
+ this.metrics.set(metricId, metric);
126
+ this.saveReportingData();
127
+
128
+ console.log(`📊 Metric registered: ${metric.name}`);
129
+ }
130
+
131
+ recordMetric(metricId, value, tags = {}, timestamp = new Date()) {
132
+ try {
133
+ const metric = this.metrics.get(metricId);
134
+ if (!metric || !metric.isActive) return false;
135
+
136
+ const dataPoint = {
137
+ metricId,
138
+ value,
139
+ tags,
140
+ timestamp,
141
+ recordedAt: new Date()
142
+ };
143
+
144
+ // Store in time series data
145
+ const timeSeriesKey = `${metricId}:timeseries`;
146
+ const timeSeries = this.storage.read.from("metrics").get(timeSeriesKey) || [];
147
+ timeSeries.push(dataPoint);
148
+
149
+ // Keep only last 10000 data points per metric
150
+ if (timeSeries.length > 10000) {
151
+ timeSeries.splice(0, timeSeries.length - 10000);
152
+ }
153
+
154
+ this.storage.write.to("metrics").set(timeSeriesKey, timeSeries);
155
+
156
+ return true;
157
+ } catch (error) {
158
+ this.errorHandler.handle(error, 'ReportingManager.recordMetric');
159
+ return false;
160
+ }
161
+ }
162
+
163
+ incrementCounter(metricId, increment = 1, tags = {}) {
164
+ return this.recordMetric(metricId, increment, tags);
165
+ }
166
+
167
+ setGauge(metricId, value, tags = {}) {
168
+ return this.recordMetric(metricId, value, tags);
169
+ }
170
+
171
+ recordHistogram(metricId, value, tags = {}) {
172
+ return this.recordMetric(metricId, value, tags);
173
+ }
174
+
175
+ // ===== REPORT GENERATION =====
176
+
177
+ async generateReport(config) {
178
+ try {
179
+ const reportId = this.generateReportId();
180
+ const report = {
181
+ id: reportId,
182
+ name: config.name,
183
+ description: config.description || '',
184
+ type: config.type || 'standard', // standard, dashboard, alert
185
+ format: config.format || 'text', // text, html, json, csv
186
+ metrics: config.metrics || [],
187
+ filters: config.filters || {},
188
+ timeRange: config.timeRange || { period: '24h' },
189
+ groupBy: config.groupBy || [],
190
+ sortBy: config.sortBy || [],
191
+ limit: config.limit || 100,
192
+ generatedAt: new Date(),
193
+ generatedBy: config.generatedBy,
194
+ status: 'generating'
195
+ };
196
+
197
+ this.reports.set(reportId, report);
198
+
199
+ // Generate report data
200
+ const reportData = await this.collectReportData(report);
201
+ report.data = reportData;
202
+ report.status = 'completed';
203
+ report.completedAt = new Date();
204
+
205
+ // Format report
206
+ const formattedReport = await this.formatReport(report);
207
+ report.formattedContent = formattedReport;
208
+
209
+ this.saveReportingData();
210
+
211
+ console.log(`📋 Report generated: ${report.name}`);
212
+ return reportId;
213
+ } catch (error) {
214
+ this.errorHandler.handle(error, 'ReportingManager.generateReport');
215
+ throw error;
216
+ }
217
+ }
218
+
219
+ async collectReportData(report) {
220
+ const data = {
221
+ summary: {},
222
+ metrics: {},
223
+ timeSeries: {},
224
+ breakdown: {}
225
+ };
226
+
227
+ const timeRange = this.parseTimeRange(report.timeRange);
228
+
229
+ for (const metricId of report.metrics) {
230
+ const metric = this.metrics.get(metricId);
231
+ if (!metric) continue;
232
+
233
+ // Get time series data
234
+ const timeSeries = await this.getMetricTimeSeries(metricId, timeRange, report.filters);
235
+ data.timeSeries[metricId] = timeSeries;
236
+
237
+ // Calculate aggregated values
238
+ const aggregatedValue = this.aggregateMetricData(timeSeries, metric.aggregation);
239
+ data.metrics[metricId] = {
240
+ name: metric.name,
241
+ value: aggregatedValue,
242
+ unit: metric.unit,
243
+ change: await this.calculateMetricChange(metricId, timeRange),
244
+ trend: this.calculateTrend(timeSeries)
245
+ };
246
+
247
+ // Generate breakdown if requested
248
+ if (report.groupBy.length > 0) {
249
+ data.breakdown[metricId] = await this.generateMetricBreakdown(
250
+ metricId,
251
+ timeRange,
252
+ report.groupBy,
253
+ report.filters
254
+ );
255
+ }
256
+ }
257
+
258
+ // Generate summary
259
+ data.summary = {
260
+ totalMetrics: report.metrics.length,
261
+ timeRange: timeRange,
262
+ generatedAt: new Date(),
263
+ dataPoints: Object.values(data.timeSeries).reduce((sum, series) => sum + series.length, 0)
264
+ };
265
+
266
+ return data;
267
+ }
268
+
269
+ async getMetricTimeSeries(metricId, timeRange, filters = {}) {
270
+ const timeSeriesKey = `${metricId}:timeseries`;
271
+ const allData = this.storage.read.from("metrics").get(timeSeriesKey) || [];
272
+
273
+ // Filter by time range
274
+ let filteredData = allData.filter(point => {
275
+ const pointTime = new Date(point.timestamp);
276
+ return pointTime >= timeRange.start && pointTime <= timeRange.end;
277
+ });
278
+
279
+ // Apply additional filters
280
+ if (Object.keys(filters).length > 0) {
281
+ filteredData = filteredData.filter(point => {
282
+ return Object.entries(filters).every(([key, value]) => {
283
+ if (Array.isArray(value)) {
284
+ return value.includes(point.tags[key]);
285
+ }
286
+ return point.tags[key] === value;
287
+ });
288
+ });
289
+ }
290
+
291
+ return filteredData;
292
+ }
293
+
294
+ aggregateMetricData(timeSeries, aggregationType) {
295
+ if (timeSeries.length === 0) return 0;
296
+
297
+ const values = timeSeries.map(point => point.value);
298
+
299
+ switch (aggregationType) {
300
+ case 'sum':
301
+ return values.reduce((sum, val) => sum + val, 0);
302
+ case 'avg':
303
+ return values.reduce((sum, val) => sum + val, 0) / values.length;
304
+ case 'count':
305
+ return values.length;
306
+ case 'count_distinct':
307
+ return new Set(values).size;
308
+ case 'min':
309
+ return Math.min(...values);
310
+ case 'max':
311
+ return Math.max(...values);
312
+ case 'rate':
313
+ const timeSpan = (new Date(timeSeries[timeSeries.length - 1].timestamp) -
314
+ new Date(timeSeries[0].timestamp)) / 1000; // in seconds
315
+ return timeSpan > 0 ? values.reduce((sum, val) => sum + val, 0) / timeSpan : 0;
316
+ default:
317
+ return values[values.length - 1] || 0;
318
+ }
319
+ }
320
+
321
+ async calculateMetricChange(metricId, currentTimeRange) {
322
+ // Calculate change compared to previous period
323
+ const periodDuration = currentTimeRange.end - currentTimeRange.start;
324
+ const previousTimeRange = {
325
+ start: new Date(currentTimeRange.start.getTime() - periodDuration),
326
+ end: currentTimeRange.start
327
+ };
328
+
329
+ const currentData = await this.getMetricTimeSeries(metricId, currentTimeRange);
330
+ const previousData = await this.getMetricTimeSeries(metricId, previousTimeRange);
331
+
332
+ const metric = this.metrics.get(metricId);
333
+ const currentValue = this.aggregateMetricData(currentData, metric.aggregation);
334
+ const previousValue = this.aggregateMetricData(previousData, metric.aggregation);
335
+
336
+ if (previousValue === 0) return null;
337
+
338
+ const change = ((currentValue - previousValue) / previousValue) * 100;
339
+
340
+ return {
341
+ percentage: change,
342
+ absolute: currentValue - previousValue,
343
+ isPositive: change > 0
344
+ };
345
+ }
346
+
347
+ calculateTrend(timeSeries) {
348
+ if (timeSeries.length < 2) return 'stable';
349
+
350
+ const values = timeSeries.map(point => point.value);
351
+ const firstHalf = values.slice(0, Math.floor(values.length / 2));
352
+ const secondHalf = values.slice(Math.floor(values.length / 2));
353
+
354
+ const firstAvg = firstHalf.reduce((sum, val) => sum + val, 0) / firstHalf.length;
355
+ const secondAvg = secondHalf.reduce((sum, val) => sum + val, 0) / secondHalf.length;
356
+
357
+ const changePercent = ((secondAvg - firstAvg) / firstAvg) * 100;
358
+
359
+ if (changePercent > 5) return 'increasing';
360
+ if (changePercent < -5) return 'decreasing';
361
+ return 'stable';
362
+ }
363
+
364
+ // ===== REPORT FORMATTING =====
365
+
366
+ async formatReport(report) {
367
+ switch (report.format) {
368
+ case 'text':
369
+ return this.formatTextReport(report);
370
+ case 'html':
371
+ return this.formatHtmlReport(report);
372
+ case 'json':
373
+ return JSON.stringify(report.data, null, 2);
374
+ case 'csv':
375
+ return this.formatCsvReport(report);
376
+ default:
377
+ return this.formatTextReport(report);
378
+ }
379
+ }
380
+
381
+ formatTextReport(report) {
382
+ let content = `📊 *${report.name}*\n`;
383
+ content += `📅 ${report.data.summary.timeRange.start.toLocaleDateString()} - ${report.data.summary.timeRange.end.toLocaleDateString()}\n\n`;
384
+
385
+ // Summary
386
+ content += `📈 *Zusammenfassung:*\n`;
387
+ content += `• Metriken: ${report.data.summary.totalMetrics}\n`;
388
+ content += `• Datenpunkte: ${report.data.summary.dataPoints}\n`;
389
+ content += `• Generiert: ${report.data.summary.generatedAt.toLocaleString()}\n\n`;
390
+
391
+ // Metrics
392
+ content += `📊 *Metriken:*\n`;
393
+ Object.entries(report.data.metrics).forEach(([metricId, metricData]) => {
394
+ content += `\n*${metricData.name}:*\n`;
395
+ content += `• Wert: ${this.formatNumber(metricData.value)} ${metricData.unit}\n`;
396
+
397
+ if (metricData.change) {
398
+ const changeIcon = metricData.change.isPositive ? '📈' : '📉';
399
+ content += `• Änderung: ${changeIcon} ${metricData.change.percentage.toFixed(1)}%\n`;
400
+ }
401
+
402
+ content += `• Trend: ${this.getTrendIcon(metricData.trend)} ${metricData.trend}\n`;
403
+ });
404
+
405
+ // Breakdown
406
+ if (Object.keys(report.data.breakdown).length > 0) {
407
+ content += `\n📋 *Aufschlüsselung:*\n`;
408
+ Object.entries(report.data.breakdown).forEach(([metricId, breakdown]) => {
409
+ const metric = this.metrics.get(metricId);
410
+ content += `\n*${metric.name} nach Kategorien:*\n`;
411
+
412
+ breakdown.slice(0, 5).forEach((item, index) => {
413
+ content += `${index + 1}. ${item.category}: ${this.formatNumber(item.value)} ${metric.unit}\n`;
414
+ });
415
+ });
416
+ }
417
+
418
+ return content;
419
+ }
420
+
421
+ formatHtmlReport(report) {
422
+ let html = `
423
+ <html>
424
+ <head>
425
+ <title>${report.name}</title>
426
+ <style>
427
+ body { font-family: Arial, sans-serif; margin: 20px; }
428
+ .header { background: #f5f5f5; padding: 20px; border-radius: 5px; }
429
+ .metric { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
430
+ .value { font-size: 24px; font-weight: bold; color: #2196F3; }
431
+ .change.positive { color: #4CAF50; }
432
+ .change.negative { color: #f44336; }
433
+ table { width: 100%; border-collapse: collapse; margin: 10px 0; }
434
+ th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; }
435
+ th { background-color: #f2f2f2; }
436
+ </style>
437
+ </head>
438
+ <body>
439
+ <div class="header">
440
+ <h1>📊 ${report.name}</h1>
441
+ <p>📅 ${report.data.summary.timeRange.start.toLocaleDateString()} - ${report.data.summary.timeRange.end.toLocaleDateString()}</p>
442
+ </div>
443
+ `;
444
+
445
+ // Metrics
446
+ html += '<h2>📊 Metriken</h2>';
447
+ Object.entries(report.data.metrics).forEach(([metricId, metricData]) => {
448
+ const changeClass = metricData.change?.isPositive ? 'positive' : 'negative';
449
+ html += `
450
+ <div class="metric">
451
+ <h3>${metricData.name}</h3>
452
+ <div class="value">${this.formatNumber(metricData.value)} ${metricData.unit}</div>
453
+ ${metricData.change ? `<div class="change ${changeClass}">Änderung: ${metricData.change.percentage.toFixed(1)}%</div>` : ''}
454
+ <div>Trend: ${this.getTrendIcon(metricData.trend)} ${metricData.trend}</div>
455
+ </div>
456
+ `;
457
+ });
458
+
459
+ html += '</body></html>';
460
+ return html;
461
+ }
462
+
463
+ formatCsvReport(report) {
464
+ let csv = 'Metric,Value,Unit,Change %,Trend\n';
465
+
466
+ Object.entries(report.data.metrics).forEach(([metricId, metricData]) => {
467
+ csv += `"${metricData.name}",${metricData.value},"${metricData.unit}",`;
468
+ csv += `${metricData.change ? metricData.change.percentage.toFixed(2) : ''},"${metricData.trend}"\n`;
469
+ });
470
+
471
+ return csv;
472
+ }
473
+
474
+ // ===== SCHEDULED REPORTS =====
475
+
476
+ scheduleReport(config) {
477
+ try {
478
+ const scheduleId = this.generateScheduleId();
479
+ const schedule = {
480
+ id: scheduleId,
481
+ name: config.name,
482
+ reportConfig: config.reportConfig,
483
+ schedule: config.schedule, // cron expression
484
+ recipients: config.recipients || [],
485
+ isActive: true,
486
+ createdAt: new Date(),
487
+ lastRun: null,
488
+ nextRun: this.calculateNextRun(config.schedule)
489
+ };
490
+
491
+ this.scheduledReports.set(scheduleId, schedule);
492
+ this.saveReportingData();
493
+
494
+ console.log(`⏰ Report scheduled: ${schedule.name}`);
495
+ return scheduleId;
496
+ } catch (error) {
497
+ this.errorHandler.handle(error, 'ReportingManager.scheduleReport');
498
+ throw error;
499
+ }
500
+ }
501
+
502
+ async processScheduledReports() {
503
+ const now = new Date();
504
+
505
+ for (const [scheduleId, schedule] of this.scheduledReports) {
506
+ if (!schedule.isActive) continue;
507
+ if (schedule.nextRun > now) continue;
508
+
509
+ try {
510
+ // Generate report
511
+ const reportId = await this.generateReport({
512
+ ...schedule.reportConfig,
513
+ generatedBy: 'scheduler'
514
+ });
515
+
516
+ const report = this.reports.get(reportId);
517
+
518
+ // Send to recipients
519
+ await this.sendReportToRecipients(report, schedule.recipients);
520
+
521
+ // Update schedule
522
+ schedule.lastRun = now;
523
+ schedule.nextRun = this.calculateNextRun(schedule.schedule);
524
+
525
+ console.log(`📧 Scheduled report sent: ${schedule.name}`);
526
+ } catch (error) {
527
+ this.errorHandler.handle(error, 'ReportingManager.processScheduledReports');
528
+ }
529
+ }
530
+ }
531
+
532
+ async sendReportToRecipients(report, recipients) {
533
+ for (const recipient of recipients) {
534
+ try {
535
+ await this.client.sendMessage(recipient, report.formattedContent);
536
+ } catch (error) {
537
+ console.error(`Failed to send report to ${recipient}:`, error);
538
+ }
539
+ }
540
+ }
541
+
542
+ // ===== DASHBOARDS =====
543
+
544
+ createDashboard(config) {
545
+ try {
546
+ const dashboardId = this.generateDashboardId();
547
+ const dashboard = {
548
+ id: dashboardId,
549
+ name: config.name,
550
+ description: config.description || '',
551
+ widgets: config.widgets || [],
552
+ layout: config.layout || 'grid',
553
+ refreshInterval: config.refreshInterval || 300000, // 5 minutes
554
+ isPublic: config.isPublic || false,
555
+ createdAt: new Date(),
556
+ createdBy: config.createdBy,
557
+ lastUpdated: new Date()
558
+ };
559
+
560
+ this.dashboards.set(dashboardId, dashboard);
561
+ this.saveReportingData();
562
+
563
+ console.log(`📊 Dashboard created: ${dashboard.name}`);
564
+ return dashboardId;
565
+ } catch (error) {
566
+ this.errorHandler.handle(error, 'ReportingManager.createDashboard');
567
+ throw error;
568
+ }
569
+ }
570
+
571
+ async updateDashboard(dashboardId) {
572
+ const dashboard = this.dashboards.get(dashboardId);
573
+ if (!dashboard) return null;
574
+
575
+ const updatedWidgets = [];
576
+
577
+ for (const widget of dashboard.widgets) {
578
+ const widgetData = await this.generateWidgetData(widget);
579
+ updatedWidgets.push({
580
+ ...widget,
581
+ data: widgetData,
582
+ lastUpdated: new Date()
583
+ });
584
+ }
585
+
586
+ dashboard.widgets = updatedWidgets;
587
+ dashboard.lastUpdated = new Date();
588
+
589
+ this.saveReportingData();
590
+ return dashboard;
591
+ }
592
+
593
+ async generateWidgetData(widget) {
594
+ const timeRange = this.parseTimeRange(widget.timeRange || { period: '24h' });
595
+
596
+ switch (widget.type) {
597
+ case 'metric':
598
+ return await this.generateMetricWidget(widget, timeRange);
599
+ case 'chart':
600
+ return await this.generateChartWidget(widget, timeRange);
601
+ case 'table':
602
+ return await this.generateTableWidget(widget, timeRange);
603
+ default:
604
+ return null;
605
+ }
606
+ }
607
+
608
+ // ===== ALERTS =====
609
+
610
+ createAlert(config) {
611
+ try {
612
+ const alertId = this.generateAlertId();
613
+ const alert = {
614
+ id: alertId,
615
+ name: config.name,
616
+ description: config.description || '',
617
+ metricId: config.metricId,
618
+ condition: config.condition, // { operator: 'gt', value: 100 }
619
+ threshold: config.threshold,
620
+ severity: config.severity || 'medium', // low, medium, high, critical
621
+ recipients: config.recipients || [],
622
+ isActive: true,
623
+ cooldownPeriod: config.cooldownPeriod || 300000, // 5 minutes
624
+ lastTriggered: null,
625
+ createdAt: new Date()
626
+ };
627
+
628
+ this.alerts.set(alertId, alert);
629
+ this.saveReportingData();
630
+
631
+ console.log(`🚨 Alert created: ${alert.name}`);
632
+ return alertId;
633
+ } catch (error) {
634
+ this.errorHandler.handle(error, 'ReportingManager.createAlert');
635
+ throw error;
636
+ }
637
+ }
638
+
639
+ async checkAlerts() {
640
+ const now = new Date();
641
+
642
+ for (const [alertId, alert] of this.alerts) {
643
+ if (!alert.isActive) continue;
644
+
645
+ // Check cooldown period
646
+ if (alert.lastTriggered && (now - alert.lastTriggered) < alert.cooldownPeriod) {
647
+ continue;
648
+ }
649
+
650
+ try {
651
+ const shouldTrigger = await this.evaluateAlertCondition(alert);
652
+
653
+ if (shouldTrigger) {
654
+ await this.triggerAlert(alert);
655
+ alert.lastTriggered = now;
656
+ }
657
+ } catch (error) {
658
+ this.errorHandler.handle(error, 'ReportingManager.checkAlerts');
659
+ }
660
+ }
661
+ }
662
+
663
+ async evaluateAlertCondition(alert) {
664
+ const timeRange = this.parseTimeRange({ period: '5m' }); // Last 5 minutes
665
+ const timeSeries = await this.getMetricTimeSeries(alert.metricId, timeRange);
666
+
667
+ if (timeSeries.length === 0) return false;
668
+
669
+ const metric = this.metrics.get(alert.metricId);
670
+ const currentValue = this.aggregateMetricData(timeSeries, metric.aggregation);
671
+
672
+ switch (alert.condition.operator) {
673
+ case 'gt':
674
+ return currentValue > alert.condition.value;
675
+ case 'gte':
676
+ return currentValue >= alert.condition.value;
677
+ case 'lt':
678
+ return currentValue < alert.condition.value;
679
+ case 'lte':
680
+ return currentValue <= alert.condition.value;
681
+ case 'eq':
682
+ return currentValue === alert.condition.value;
683
+ case 'ne':
684
+ return currentValue !== alert.condition.value;
685
+ default:
686
+ return false;
687
+ }
688
+ }
689
+
690
+ async triggerAlert(alert) {
691
+ const metric = this.metrics.get(alert.metricId);
692
+ const severityIcon = this.getSeverityIcon(alert.severity);
693
+
694
+ const message = `${severityIcon} *ALERT: ${alert.name}*\n\n` +
695
+ `📊 Metrik: ${metric.name}\n` +
696
+ `⚠️ Bedingung: ${alert.condition.operator} ${alert.condition.value}\n` +
697
+ `🕐 Zeit: ${new Date().toLocaleString()}\n\n` +
698
+ `${alert.description}`;
699
+
700
+ // Send to recipients
701
+ for (const recipient of alert.recipients) {
702
+ try {
703
+ await this.client.sendMessage(recipient, message);
704
+ } catch (error) {
705
+ console.error(`Failed to send alert to ${recipient}:`, error);
706
+ }
707
+ }
708
+
709
+ console.log(`🚨 Alert triggered: ${alert.name}`);
710
+ }
711
+
712
+ // ===== UTILITY METHODS =====
713
+
714
+ parseTimeRange(timeRange) {
715
+ const now = new Date();
716
+ let start, end;
717
+
718
+ if (timeRange.start && timeRange.end) {
719
+ start = new Date(timeRange.start);
720
+ end = new Date(timeRange.end);
721
+ } else if (timeRange.period) {
722
+ const period = timeRange.period;
723
+ const match = period.match(/^(\d+)([hdwmy])$/);
724
+
725
+ if (match) {
726
+ const value = parseInt(match[1]);
727
+ const unit = match[2];
728
+
729
+ let milliseconds;
730
+ switch (unit) {
731
+ case 'h': milliseconds = value * 60 * 60 * 1000; break;
732
+ case 'd': milliseconds = value * 24 * 60 * 60 * 1000; break;
733
+ case 'w': milliseconds = value * 7 * 24 * 60 * 60 * 1000; break;
734
+ case 'm': milliseconds = value * 30 * 24 * 60 * 60 * 1000; break;
735
+ case 'y': milliseconds = value * 365 * 24 * 60 * 60 * 1000; break;
736
+ default: milliseconds = 24 * 60 * 60 * 1000; // 1 day
737
+ }
738
+
739
+ start = new Date(now.getTime() - milliseconds);
740
+ end = now;
741
+ } else {
742
+ // Default to last 24 hours
743
+ start = new Date(now.getTime() - 24 * 60 * 60 * 1000);
744
+ end = now;
745
+ }
746
+ } else {
747
+ // Default to last 24 hours
748
+ start = new Date(now.getTime() - 24 * 60 * 60 * 1000);
749
+ end = now;
750
+ }
751
+
752
+ return { start, end };
753
+ }
754
+
755
+ formatNumber(num) {
756
+ if (num >= 1000000) {
757
+ return (num / 1000000).toFixed(1) + 'M';
758
+ } else if (num >= 1000) {
759
+ return (num / 1000).toFixed(1) + 'K';
760
+ }
761
+ return num.toFixed(0);
762
+ }
763
+
764
+ getTrendIcon(trend) {
765
+ switch (trend) {
766
+ case 'increasing': return '📈';
767
+ case 'decreasing': return '📉';
768
+ case 'stable': return '➡️';
769
+ default: return '📊';
770
+ }
771
+ }
772
+
773
+ getSeverityIcon(severity) {
774
+ switch (severity) {
775
+ case 'low': return '🟡';
776
+ case 'medium': return '🟠';
777
+ case 'high': return '🔴';
778
+ case 'critical': return '🚨';
779
+ default: return '⚠️';
780
+ }
781
+ }
782
+
783
+ calculateNextRun(cronExpression) {
784
+ // Simple cron parser - in production, use a proper cron library
785
+ const now = new Date();
786
+ return new Date(now.getTime() + 60 * 60 * 1000); // Next hour as fallback
787
+ }
788
+
789
+ generateReportId() {
790
+ return `report_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
791
+ }
792
+
793
+ generateScheduleId() {
794
+ return `schedule_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
795
+ }
796
+
797
+ generateDashboardId() {
798
+ return `dashboard_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
799
+ }
800
+
801
+ generateAlertId() {
802
+ return `alert_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
803
+ }
804
+
805
+ updateRealTimeMetrics() {
806
+ // Update real-time metrics like active users, response times, etc.
807
+ // This would integrate with your actual application metrics
808
+
809
+ // Example: Record current active users
810
+ const activeUsers = this.client.getActiveUsersCount?.() || 0;
811
+ this.setGauge('active_users', activeUsers);
812
+
813
+ // Example: Record response time
814
+ const responseTime = this.client.getAverageResponseTime?.() || 0;
815
+ this.recordHistogram('response_time', responseTime);
816
+ }
817
+
818
+ saveReportingData() {
819
+ try {
820
+ const reportingData = {
821
+ reports: Object.fromEntries(this.reports),
822
+ scheduledReports: Object.fromEntries(this.scheduledReports),
823
+ dashboards: Object.fromEntries(this.dashboards),
824
+ metrics: Object.fromEntries(this.metrics),
825
+ alerts: Object.fromEntries(this.alerts)
826
+ };
827
+
828
+ this.storage.write.to("reporting").set("data", reportingData);
829
+ } catch (error) {
830
+ this.errorHandler.handle(error, 'ReportingManager.saveReportingData');
831
+ }
832
+ }
833
+
834
+ // ===== PUBLIC API =====
835
+
836
+ getAllReports() {
837
+ return Array.from(this.reports.values());
838
+ }
839
+
840
+ getReport(reportId) {
841
+ return this.reports.get(reportId);
842
+ }
843
+
844
+ getAllDashboards() {
845
+ return Array.from(this.dashboards.values());
846
+ }
847
+
848
+ getDashboard(dashboardId) {
849
+ return this.dashboards.get(dashboardId);
850
+ }
851
+
852
+ getAllMetrics() {
853
+ return Array.from(this.metrics.values());
854
+ }
855
+
856
+ getMetric(metricId) {
857
+ return this.metrics.get(metricId);
858
+ }
859
+
860
+ getAllAlerts() {
861
+ return Array.from(this.alerts.values());
862
+ }
863
+
864
+ getAlert(alertId) {
865
+ return this.alerts.get(alertId);
866
+ }
867
+ }