bulltrackers-module 1.0.679 → 1.0.681
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.
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
7
7
|
const zlib = require('zlib');
|
|
8
8
|
const { Storage } = require('@google-cloud/storage');
|
|
9
|
-
const {
|
|
10
|
-
//
|
|
9
|
+
const { generateAlertMessage } = require('./alert_manifest_loader');
|
|
10
|
+
// [UPDATED] Now uses dynamic manifest loading instead of hardcoded registry
|
|
11
11
|
|
|
12
12
|
const storage = new Storage(); // Singleton GCS Client
|
|
13
13
|
|
|
@@ -222,21 +222,13 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
222
222
|
async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computationDate, dependencies = {}) {
|
|
223
223
|
const subscriptions = [];
|
|
224
224
|
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
'NewSectorExposure': 'newSector',
|
|
230
|
-
'PositionInvestedIncrease': 'increasedPositionSize',
|
|
231
|
-
'NewSocialPost': 'newSocialPost',
|
|
232
|
-
// [NEW] Mapping for BehavioralAnomaly
|
|
233
|
-
'BehavioralAnomaly': 'behavioralAnomaly',
|
|
234
|
-
'TestSystemProbe': 'increasedRisk' // Hack: Map to 'increasedRisk' key
|
|
235
|
-
};
|
|
225
|
+
// [DYNAMIC] Get configKey from alertType metadata instead of hardcoded mapping
|
|
226
|
+
// The alertType is passed in dependencies and contains the configKey from computation metadata
|
|
227
|
+
const alertType = dependencies.alertType;
|
|
228
|
+
const configKey = alertType?.configKey;
|
|
236
229
|
|
|
237
|
-
const configKey = computationToConfigKey[alertTypeId];
|
|
238
230
|
if (!configKey) {
|
|
239
|
-
logger.log('WARN', `[findSubscriptionsForPI] No
|
|
231
|
+
logger.log('WARN', `[findSubscriptionsForPI] No configKey found for alert type: ${alertTypeId}`);
|
|
240
232
|
return subscriptions;
|
|
241
233
|
}
|
|
242
234
|
|
|
@@ -264,8 +256,8 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
|
|
|
264
256
|
increasedPositionSize: true,
|
|
265
257
|
newSocialPost: true,
|
|
266
258
|
newPositions: true,
|
|
267
|
-
|
|
268
|
-
|
|
259
|
+
behavioralAnomaly: true,
|
|
260
|
+
testSystemProbe: true // Test alerts for developers
|
|
269
261
|
};
|
|
270
262
|
|
|
271
263
|
// Check all developer accounts
|
|
@@ -364,9 +356,13 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
|
|
|
364
356
|
const isTestProbe = alertTypeId === 'TestSystemProbe';
|
|
365
357
|
const isEnabled = item.alertConfig && item.alertConfig[configKey] === true;
|
|
366
358
|
|
|
367
|
-
// [FIX]
|
|
359
|
+
// [FIX] Check if this is a test alert (respect testAlerts preference)
|
|
360
|
+
// Load alert type from dependencies to check isTest flag
|
|
361
|
+
const alertType = dependencies.alertType;
|
|
362
|
+
const isTestAlert = alertType && alertType.isTest === true;
|
|
363
|
+
|
|
368
364
|
let shouldSendAlert = false;
|
|
369
|
-
if (
|
|
365
|
+
if (isTestAlert) {
|
|
370
366
|
// Check if user has testAlerts enabled in their notification preferences
|
|
371
367
|
try {
|
|
372
368
|
const { manageNotificationPreferences } = require('../../api-v2/helpers/data-fetchers/firestore.js');
|
|
@@ -374,7 +370,7 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
|
|
|
374
370
|
shouldSendAlert = prefs.testAlerts === true;
|
|
375
371
|
|
|
376
372
|
if (!shouldSendAlert) {
|
|
377
|
-
logger.log('DEBUG', `[findSubscriptionsForPI] User ${userCid} has testAlerts disabled, skipping
|
|
373
|
+
logger.log('DEBUG', `[findSubscriptionsForPI] User ${userCid} has testAlerts disabled, skipping test alert ${alertTypeId}`);
|
|
378
374
|
}
|
|
379
375
|
} catch (prefError) {
|
|
380
376
|
logger.log('WARN', `[findSubscriptionsForPI] Error checking testAlerts preference for user ${userCid}: ${prefError.message}`);
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Dynamic Alert Manifest Loader
|
|
3
|
+
* Loads alert types from computation classes instead of hardcoded registry
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Flatten nested calculations object (same logic as index.js)
|
|
8
|
+
* @param {Object} obj - Nested calculations object
|
|
9
|
+
* @returns {Array<Function>} Flat array of computation classes
|
|
10
|
+
*/
|
|
11
|
+
const flattenCalculations = (obj) => {
|
|
12
|
+
let result = [];
|
|
13
|
+
for (const key in obj) {
|
|
14
|
+
if (typeof obj[key] === 'function') {
|
|
15
|
+
result.push(obj[key]);
|
|
16
|
+
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
17
|
+
result = result.concat(flattenCalculations(obj[key]));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load all alert computations from calculations package
|
|
25
|
+
* @param {Object} logger - Logger instance
|
|
26
|
+
* @returns {Promise<Array>} Array of alert type objects with metadata
|
|
27
|
+
*/
|
|
28
|
+
async function loadAlertTypesFromManifest(logger) {
|
|
29
|
+
try {
|
|
30
|
+
// Import calculations the same way index.js does
|
|
31
|
+
const { calculations } = require('aiden-shared-calculations-unified');
|
|
32
|
+
const calculationsArray = flattenCalculations(calculations);
|
|
33
|
+
|
|
34
|
+
const alertTypes = [];
|
|
35
|
+
|
|
36
|
+
// Iterate through all computation classes (same logic as index.js alert trigger registration)
|
|
37
|
+
for (const CalcClass of calculationsArray) {
|
|
38
|
+
if (!CalcClass || typeof CalcClass.getMetadata !== 'function') {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const metadata = CalcClass.getMetadata();
|
|
43
|
+
|
|
44
|
+
// Check if this is an alert computation
|
|
45
|
+
if (metadata.isAlertComputation !== true) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check if alert metadata exists
|
|
50
|
+
if (!metadata.alert) {
|
|
51
|
+
logger?.log('WARN', `[AlertManifestLoader] Computation ${metadata.name} has isAlertComputation=true but no alert metadata`);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Build alert type object
|
|
56
|
+
const alertType = {
|
|
57
|
+
id: metadata.alert.id,
|
|
58
|
+
name: metadata.alert.frontendName,
|
|
59
|
+
description: metadata.alert.description,
|
|
60
|
+
computationName: metadata.name,
|
|
61
|
+
category: metadata.category || 'alerts',
|
|
62
|
+
messageTemplate: metadata.alert.messageTemplate,
|
|
63
|
+
severity: metadata.alert.severity || 'medium',
|
|
64
|
+
configKey: metadata.alert.configKey,
|
|
65
|
+
isTest: metadata.isTest === true,
|
|
66
|
+
enabled: true
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
alertTypes.push(alertType);
|
|
70
|
+
|
|
71
|
+
logger?.log('DEBUG', `[AlertManifestLoader] Loaded alert type: ${alertType.id} from ${metadata.name}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
logger?.log('INFO', `[AlertManifestLoader] Successfully loaded ${alertTypes.length} alert types from calculations`);
|
|
75
|
+
|
|
76
|
+
return alertTypes;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
logger?.log('ERROR', `[AlertManifestLoader] Failed to load alert types: ${error.message}`);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get alert type by computation name
|
|
85
|
+
* @param {Array} alertTypes - Array of alert types from loadAlertTypesFromManifest
|
|
86
|
+
* @param {string} computationName - Name of computation
|
|
87
|
+
* @returns {Object|null} Alert type object or null
|
|
88
|
+
*/
|
|
89
|
+
function getAlertTypeByComputation(alertTypes, computationName) {
|
|
90
|
+
return alertTypes.find(type => type.computationName === computationName) || null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if a computation is an alert computation
|
|
95
|
+
* @param {Array} alertTypes - Array of alert types from loadAlertTypesFromManifest
|
|
96
|
+
* @param {string} computationName - Name of computation
|
|
97
|
+
* @returns {boolean} True if computation is an alert
|
|
98
|
+
*/
|
|
99
|
+
function isAlertComputation(alertTypes, computationName) {
|
|
100
|
+
return getAlertTypeByComputation(alertTypes, computationName) !== null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Generate alert message from template and metadata
|
|
105
|
+
* @param {Object} alertType - Alert type object
|
|
106
|
+
* @param {string} piUsername - PI username
|
|
107
|
+
* @param {Object} metadata - Metadata from computation results
|
|
108
|
+
* @returns {string} Generated alert message
|
|
109
|
+
*/
|
|
110
|
+
function generateAlertMessage(alertType, piUsername, metadata = {}) {
|
|
111
|
+
let message = alertType.messageTemplate;
|
|
112
|
+
|
|
113
|
+
// Replace placeholders
|
|
114
|
+
message = message.replace(/{piUsername}/g, piUsername || 'Unknown');
|
|
115
|
+
message = message.replace(/{count}/g, metadata.count || metadata.positions?.length || metadata.moveCount || 0);
|
|
116
|
+
message = message.replace(/{change}/g, metadata.change || metadata.changePercent || metadata.diff || 'N/A');
|
|
117
|
+
message = message.replace(/{previous}/g, metadata.previous || metadata.previousValue || metadata.prev || metadata.previousRisk || 'N/A');
|
|
118
|
+
message = message.replace(/{current}/g, metadata.current || metadata.currentValue || metadata.curr || metadata.currentRisk || 'N/A');
|
|
119
|
+
message = message.replace(/{sectorName}/g, metadata.sectorName || (metadata.newExposures && metadata.newExposures.length > 0 ? metadata.newExposures.join(', ') : 'Unknown'));
|
|
120
|
+
message = message.replace(/{ticker}/g, metadata.ticker || metadata.symbol || 'Unknown');
|
|
121
|
+
|
|
122
|
+
// Format numeric values
|
|
123
|
+
message = message.replace(/{volatility}/g, metadata.volatility ? `${(metadata.volatility * 100).toFixed(1)}` : 'N/A');
|
|
124
|
+
message = message.replace(/{threshold}/g, metadata.threshold ? `${(metadata.threshold * 100).toFixed(0)}` : 'N/A');
|
|
125
|
+
message = message.replace(/{diff}/g, metadata.diff ? `${metadata.diff.toFixed(1)}` : 'N/A');
|
|
126
|
+
message = message.replace(/{prev}/g, metadata.prev ? `${metadata.prev.toFixed(1)}` : 'N/A');
|
|
127
|
+
message = message.replace(/{curr}/g, metadata.curr ? `${metadata.curr.toFixed(1)}` : 'N/A');
|
|
128
|
+
message = message.replace(/{title}/g, metadata.title || 'New Update');
|
|
129
|
+
|
|
130
|
+
// Probe placeholders
|
|
131
|
+
message = message.replace(/{status}/g, metadata.status || 'Unknown Status');
|
|
132
|
+
message = message.replace(/{timestamp}/g, metadata.timestamp || new Date().toISOString());
|
|
133
|
+
|
|
134
|
+
// Behavioral Anomaly placeholders
|
|
135
|
+
message = message.replace(/{primaryDriver}/g, metadata.primaryDriver || 'Unknown Factor');
|
|
136
|
+
message = message.replace(/{driverSignificance}/g, metadata.driverSignificance || 'N/A');
|
|
137
|
+
message = message.replace(/{anomalyScore}/g, metadata.anomalyScore || 'N/A');
|
|
138
|
+
|
|
139
|
+
// Handle positions list if available
|
|
140
|
+
if (metadata.positions && Array.isArray(metadata.positions) && metadata.positions.length > 0) {
|
|
141
|
+
const positionsList = metadata.positions
|
|
142
|
+
.slice(0, 3)
|
|
143
|
+
.map(p => p.ticker || p.instrumentId)
|
|
144
|
+
.join(', ');
|
|
145
|
+
message = message.replace(/{positions}/g, positionsList);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Handle moves array for PositionInvestedIncrease
|
|
149
|
+
if (metadata.moves && Array.isArray(metadata.moves) && metadata.moves.length > 0) {
|
|
150
|
+
const firstMove = metadata.moves[0];
|
|
151
|
+
message = message.replace(/{symbol}/g, firstMove.symbol || 'Unknown');
|
|
152
|
+
message = message.replace(/{diff}/g, firstMove.diff ? `${firstMove.diff.toFixed(1)}` : 'N/A');
|
|
153
|
+
message = message.replace(/{prev}/g, firstMove.prev ? `${firstMove.prev.toFixed(1)}` : 'N/A');
|
|
154
|
+
message = message.replace(/{curr}/g, firstMove.curr ? `${firstMove.curr.toFixed(1)}` : 'N/A');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return message;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
loadAlertTypesFromManifest,
|
|
162
|
+
getAlertTypeByComputation,
|
|
163
|
+
isAlertComputation,
|
|
164
|
+
generateAlertMessage
|
|
165
|
+
};
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Alert System Trigger Handler
|
|
3
3
|
* Can be triggered via Pub/Sub when computation results are written
|
|
4
|
+
* [UPDATED] Now uses dynamic manifest loading instead of hardcoded registry
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
const { getAlertTypeByComputation, isAlertComputation } = require('./helpers/
|
|
7
|
+
const { loadAlertTypesFromManifest, getAlertTypeByComputation, isAlertComputation } = require('./helpers/alert_manifest_loader');
|
|
7
8
|
const { processAlertForPI, readComputationResults, readComputationResultsWithShards } = require('./helpers/alert_helpers');
|
|
8
9
|
|
|
10
|
+
// Cache for loaded alert types (loaded once per function instance)
|
|
11
|
+
let cachedAlertTypes = null;
|
|
12
|
+
|
|
9
13
|
/**
|
|
10
14
|
* Pub/Sub trigger handler for alert generation
|
|
11
15
|
* Expected message format:
|
|
@@ -19,6 +23,11 @@ async function handleAlertTrigger(message, context, config, dependencies) {
|
|
|
19
23
|
const { db, logger } = dependencies;
|
|
20
24
|
|
|
21
25
|
try {
|
|
26
|
+
// [NEW] Load alert types from manifest (cached)
|
|
27
|
+
if (!cachedAlertTypes) {
|
|
28
|
+
cachedAlertTypes = await loadAlertTypesFromManifest(logger);
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
// Parse Pub/Sub message
|
|
23
32
|
let payload;
|
|
24
33
|
if (message.data) {
|
|
@@ -35,13 +44,13 @@ async function handleAlertTrigger(message, context, config, dependencies) {
|
|
|
35
44
|
return;
|
|
36
45
|
}
|
|
37
46
|
|
|
38
|
-
// 1. Check if this is an alert computation
|
|
39
|
-
if (!isAlertComputation(computationName)) {
|
|
47
|
+
// 1. Check if this is an alert computation (using dynamic manifest)
|
|
48
|
+
if (!isAlertComputation(cachedAlertTypes, computationName)) {
|
|
40
49
|
logger.log('DEBUG', `[AlertTrigger] Not an alert computation: ${computationName}`);
|
|
41
50
|
return;
|
|
42
51
|
}
|
|
43
52
|
|
|
44
|
-
const alertType = getAlertTypeByComputation(computationName);
|
|
53
|
+
const alertType = getAlertTypeByComputation(cachedAlertTypes, computationName);
|
|
45
54
|
if (!alertType) {
|
|
46
55
|
logger.log('WARN', `[AlertTrigger] Alert type not found for computation: ${computationName}`);
|
|
47
56
|
return;
|
|
@@ -101,7 +110,7 @@ async function handleAlertTrigger(message, context, config, dependencies) {
|
|
|
101
110
|
alertType,
|
|
102
111
|
piMetadata,
|
|
103
112
|
date,
|
|
104
|
-
dependencies
|
|
113
|
+
{ ...dependencies, alertType } // Pass alertType in dependencies for isTest check
|
|
105
114
|
);
|
|
106
115
|
processedCount++;
|
|
107
116
|
} catch (error) {
|
|
@@ -134,47 +143,55 @@ async function handleAlertTrigger(message, context, config, dependencies) {
|
|
|
134
143
|
*/
|
|
135
144
|
async function handleComputationResultWrite(change, context, config, dependencies) {
|
|
136
145
|
const { db, logger } = dependencies;
|
|
146
|
+
|
|
147
|
+
// Declare variables outside try block for error logging
|
|
148
|
+
let date, category, computationName;
|
|
137
149
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
150
|
+
try {
|
|
151
|
+
// [NEW] Load alert types from manifest (cached)
|
|
152
|
+
if (!cachedAlertTypes) {
|
|
153
|
+
cachedAlertTypes = await loadAlertTypesFromManifest(logger);
|
|
154
|
+
}
|
|
142
155
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
156
|
+
// --- PATH RESOLUTION LOGIC ---
|
|
157
|
+
// 1. Try to use context.params (Preferred for Gen 2 & Wildcards)
|
|
158
|
+
// The 'contextShim' in your root index.js passes 'event.params' into here.
|
|
159
|
+
({ date, category, computationName } = context.params || {});
|
|
160
|
+
|
|
161
|
+
// 2. Fallback: Parse from resource string (Legacy / Backup)
|
|
162
|
+
if (!date || !computationName) {
|
|
163
|
+
const resource = context.resource || (change.after && change.after.ref.path) || '';
|
|
164
|
+
|
|
165
|
+
// Simple split logic can fail on full resource URIs (e.g. //firestore.googleapis.com/...)
|
|
166
|
+
// so we look for specific keywords to anchor the split.
|
|
167
|
+
const pathParts = resource.split('/');
|
|
168
|
+
|
|
169
|
+
const dateIndex = pathParts.indexOf('unified_insights') + 1;
|
|
170
|
+
const categoryIndex = pathParts.indexOf('results') + 1;
|
|
171
|
+
const computationIndex = pathParts.indexOf('computations') + 1;
|
|
172
|
+
|
|
173
|
+
if (dateIndex > 0 && categoryIndex > 0 && computationIndex > 0) {
|
|
174
|
+
date = pathParts[dateIndex];
|
|
175
|
+
category = pathParts[categoryIndex];
|
|
176
|
+
computationName = pathParts[computationIndex];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
161
179
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
180
|
+
// 3. Validation
|
|
181
|
+
if (!date || !category || !computationName) {
|
|
182
|
+
logger.log('WARN', `[AlertTrigger] Could not resolve path params. Resource: ${context.resource}`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Only process if document was created or updated (not deleted)
|
|
187
|
+
if (!change.after.exists) {
|
|
188
|
+
logger.log('INFO', `[AlertTrigger] Document deleted, skipping: ${computationName} for ${date}`);
|
|
165
189
|
return;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (!change.after.exists) {
|
|
170
|
-
logger.log('INFO', `[AlertTrigger] Document deleted, skipping: ${computationName} for ${date}`);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
// 1. Check if this is an alert computation OR PopularInvestorProfileMetrics
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 1. Check if this is an alert computation OR PopularInvestorProfileMetrics (using dynamic manifest)
|
|
176
193
|
const isProfileMetrics = computationName === 'PopularInvestorProfileMetrics';
|
|
177
|
-
if (!isAlertComputation(computationName) && !isProfileMetrics) {
|
|
194
|
+
if (!isAlertComputation(cachedAlertTypes, computationName) && !isProfileMetrics) {
|
|
178
195
|
logger.log('DEBUG', `[AlertTrigger] Not an alert computation or profile metrics: ${computationName}`);
|
|
179
196
|
return;
|
|
180
197
|
}
|
|
@@ -195,13 +212,18 @@ async function handleComputationResultWrite(change, context, config, dependencie
|
|
|
195
212
|
return; // Don't process as alert computation
|
|
196
213
|
}
|
|
197
214
|
|
|
198
|
-
const alertType = getAlertTypeByComputation(computationName);
|
|
215
|
+
const alertType = getAlertTypeByComputation(cachedAlertTypes, computationName);
|
|
199
216
|
if (!alertType) {
|
|
200
217
|
logger.log('WARN', `[AlertTrigger] Alert type not found for computation: ${computationName}`);
|
|
201
218
|
return;
|
|
202
219
|
}
|
|
203
220
|
|
|
204
|
-
|
|
221
|
+
// [NEW] Filter test alerts - only send to developers if isTest === true
|
|
222
|
+
if (alertType.isTest) {
|
|
223
|
+
logger.log('INFO', `[AlertTrigger] Processing TEST alert computation: ${computationName} for date ${date} (will only send to developers)`);
|
|
224
|
+
} else {
|
|
225
|
+
logger.log('INFO', `[AlertTrigger] Processing alert computation: ${computationName} for date ${date}`);
|
|
226
|
+
}
|
|
205
227
|
|
|
206
228
|
// 2. Read and decompress computation results (handling GCS, shards, and compression)
|
|
207
229
|
const docData = change.after.data();
|
|
@@ -233,7 +255,7 @@ async function handleComputationResultWrite(change, context, config, dependencie
|
|
|
233
255
|
alertType,
|
|
234
256
|
piMetadata,
|
|
235
257
|
date,
|
|
236
|
-
dependencies
|
|
258
|
+
{ ...dependencies, alertType } // Pass alertType in dependencies for isTest check
|
|
237
259
|
);
|
|
238
260
|
processedCount++;
|
|
239
261
|
} catch (error) {
|
|
@@ -6,9 +6,11 @@ const {
|
|
|
6
6
|
fetchAlertTypes,
|
|
7
7
|
getUserSubscriptions,
|
|
8
8
|
updateSubscription,
|
|
9
|
-
unsubscribeFromAlerts
|
|
9
|
+
unsubscribeFromAlerts,
|
|
10
|
+
isDeveloper
|
|
10
11
|
} = require('../helpers/data-fetchers/firestore.js');
|
|
11
12
|
const { sanitizeCid, sanitizeDocId, validateBatchSize } = require('../helpers/security_utils.js');
|
|
13
|
+
const { loadAlertTypesFromManifest } = require('../../alert-system/helpers/alert_manifest_loader');
|
|
12
14
|
|
|
13
15
|
const router = express.Router();
|
|
14
16
|
|
|
@@ -34,6 +36,43 @@ const updateSubscriptionSchema = z.object({
|
|
|
34
36
|
thresholds: z.record(z.any()).optional()
|
|
35
37
|
});
|
|
36
38
|
|
|
39
|
+
// [NEW] GET /alerts/types - Dynamically load available alert types from manifest
|
|
40
|
+
router.get('/types', async (req, res, next) => {
|
|
41
|
+
try {
|
|
42
|
+
const { db, logger } = req.dependencies;
|
|
43
|
+
|
|
44
|
+
// Load alert types from manifest
|
|
45
|
+
const alertTypes = await loadAlertTypesFromManifest(logger);
|
|
46
|
+
|
|
47
|
+
// Check if user is a developer
|
|
48
|
+
const isDev = await isDeveloper(db, String(req.targetUserId));
|
|
49
|
+
|
|
50
|
+
// Filter out test alerts for non-developers
|
|
51
|
+
const filteredAlertTypes = isDev
|
|
52
|
+
? alertTypes
|
|
53
|
+
: alertTypes.filter(type => !type.isTest);
|
|
54
|
+
|
|
55
|
+
// Format response for frontend
|
|
56
|
+
const formattedTypes = filteredAlertTypes.map(type => ({
|
|
57
|
+
id: type.id,
|
|
58
|
+
name: type.name,
|
|
59
|
+
description: type.description,
|
|
60
|
+
severity: type.severity,
|
|
61
|
+
configKey: type.configKey,
|
|
62
|
+
isTest: type.isTest || false
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
res.json({
|
|
66
|
+
success: true,
|
|
67
|
+
alertTypes: formattedTypes,
|
|
68
|
+
count: formattedTypes.length
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger?.log('ERROR', `[GET /alerts/types] Error loading alert types: ${error.message}`);
|
|
72
|
+
next(error);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
37
76
|
// GET /alerts/history
|
|
38
77
|
router.get('/history', async (req, res, next) => {
|
|
39
78
|
try {
|