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.
- package/CHANGELOG.md +29 -0
- package/README.md +34 -3
- package/package.json +3 -2
- package/src/ab-testing.js +698 -0
- package/src/advanced-scheduler.js +577 -0
- package/src/ai-features.js +459 -0
- package/src/ai-integration.js +2 -1
- package/src/analytics-manager.js +458 -0
- package/src/business-manager.js +362 -0
- package/src/client.js +447 -39
- package/src/console-logger.js +256 -0
- package/src/core.js +28 -3
- package/src/cross-platform.js +538 -0
- package/src/database-manager.js +766 -0
- package/src/device-manager.js +1 -1
- package/src/easy-bot-fixed.js +341 -0
- package/src/easy-bot.js +503 -22
- package/src/error-handler.js +230 -0
- package/src/gaming-manager.js +842 -0
- package/src/http-client.js +1 -1
- package/src/index.js +15 -0
- package/src/message.js +197 -94
- package/src/multi-client.js +26 -12
- package/src/plugin-manager.js +59 -10
- package/src/prefix-manager.js +48 -1
- package/src/qr-terminal-fix.js +239 -0
- package/src/qr.js +170 -27
- package/src/quick-bot.js +63 -0
- package/src/reporting-manager.js +867 -0
- package/src/scheduler.js +14 -1
- package/src/security-manager.js +678 -0
- package/src/session-manager-old.js +314 -0
- package/src/session-manager.js +429 -24
- package/src/storage.js +254 -194
- package/src/ui-components.js +560 -0
|
@@ -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
|
+
}
|