bulltrackers-module 1.0.493 โ†’ 1.0.494

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.
@@ -345,7 +345,13 @@ async function handleSweepDispatch(config, dependencies, computationManifest, re
345
345
  };
346
346
  });
347
347
 
348
- logger.log('WARN', `[Sweep] ๐Ÿงน Forcing ${tasksPayload.length} tasks to HIGH-MEM for ${date}.`);
348
+ const taskNames = tasksPayload.map(t => t.computation || t.name).join(', ');
349
+ logger.log('WARN', `[Sweep] ๐Ÿงน Forcing ${tasksPayload.length} tasks to HIGH-MEM for ${date}.`, {
350
+ date: date,
351
+ pass: passToRun,
352
+ tasks: tasksPayload.map(t => ({ name: t.computation || t.name, reason: 'sweep' })),
353
+ topic: config.computationTopicHighMem || 'computation-tasks-highmem'
354
+ });
349
355
 
350
356
  await pubsubUtils.batchPublishTasks(dependencies, {
351
357
  topicName: config.computationTopicHighMem || 'computation-tasks-highmem',
@@ -503,6 +509,13 @@ async function handleStandardDispatch(config, dependencies, computationManifest,
503
509
 
504
510
  const pubPromises = [];
505
511
  if (standardTasks.length > 0) {
512
+ const taskNames = standardTasks.map(t => t.computation || t.name).join(', ');
513
+ logger.log('INFO', `[Dispatcher] ๐Ÿ“ค Dispatching ${standardTasks.length} standard tasks: ${taskNames}`, {
514
+ date: selectedDate,
515
+ pass: passToRun,
516
+ tasks: standardTasks.map(t => ({ name: t.computation || t.name, reason: t.triggerReason || 'new' })),
517
+ topic: config.computationTopicStandard || 'computation-tasks'
518
+ });
506
519
  pubPromises.push(pubsubUtils.batchPublishTasks(dependencies, {
507
520
  topicName: config.computationTopicStandard || 'computation-tasks',
508
521
  tasks: standardTasks,
@@ -510,6 +523,13 @@ async function handleStandardDispatch(config, dependencies, computationManifest,
510
523
  }));
511
524
  }
512
525
  if (highMemTasks.length > 0) {
526
+ const taskNames = highMemTasks.map(t => t.computation || t.name).join(', ');
527
+ logger.log('INFO', `[Dispatcher] ๐Ÿ“ค Dispatching ${highMemTasks.length} high-memory tasks: ${taskNames}`, {
528
+ date: selectedDate,
529
+ pass: passToRun,
530
+ tasks: highMemTasks.map(t => ({ name: t.computation || t.name, reason: t.triggerReason || 'retry' })),
531
+ topic: config.computationTopicHighMem || 'computation-tasks-highmem'
532
+ });
513
533
  pubPromises.push(pubsubUtils.batchPublishTasks(dependencies, {
514
534
  topicName: config.computationTopicHighMem || 'computation-tasks-highmem',
515
535
  tasks: highMemTasks,
@@ -164,7 +164,16 @@ class FinalSweepReporter {
164
164
 
165
165
  if (ledgerSnap.exists) {
166
166
  const data = ledgerSnap.data();
167
- forensics.ledgerState = { status: data.status, workerId: data.workerId, error: data.error };
167
+ // Filter out undefined values to prevent Firestore errors
168
+ const ledgerState = {
169
+ status: data.status,
170
+ workerId: data.workerId
171
+ };
172
+ // Only include error if it's defined
173
+ if (data.error !== undefined && data.error !== null) {
174
+ ledgerState.error = data.error;
175
+ }
176
+ forensics.ledgerState = ledgerState;
168
177
 
169
178
  if (['PENDING', 'IN_PROGRESS'].includes(data.status)) {
170
179
  const lastHb = data.telemetry?.lastHeartbeat ? new Date(data.telemetry.lastHeartbeat).getTime() : 0;
@@ -1,32 +1,30 @@
1
- Below is a quick-reference guide for testing the **Data Feeder Pipeline** using the Google Cloud Console UI.
2
-
3
- ---
4
-
5
1
  # ๐Ÿงช Workflow Testing Guide
6
2
 
7
- When triggering a manual execution in the **GCP Workflows Console**, use the following JSON objects in the **Input** field to bypass the schedule and test specific components.
3
+ Below is a quick-reference guide for manually triggering the **Data Feeder Pipeline** using the Google Cloud Console UI.
8
4
 
9
- ### How to Run a Test
5
+ ## How to Run a Test
10
6
 
11
- 1. Go to the **Workflows** page in your GCP Console.
12
- 2. Click on `data-feeder-pipeline`.
7
+ 1. Navigate to **Workflows** in your GCP Console.
8
+ 2. Select `data-feeder-pipeline`.
13
9
  3. Click the **Execute** button at the top.
14
- 4. Paste the relevant **JSON Input** from the table below into the input box.
10
+ 4. Paste the specific **JSON Input** from the table below into the input box to bypass schedules and target specific phases.
15
11
 
16
12
  ### Test Commands
17
13
 
18
- | Component to Test | JSON Input | Description |
19
- | --- | --- | --- |
20
- | **Market Data** | `{"target_step": "market"}` | Runs `price-fetcher`, `insights-fetcher`, and `market-data-indexer`. |
21
- | **Rankings** | `{"target_step": "rankings"}` | Runs the `fetch-popular-investors` function specifically. |
22
- | **Social Orchestrator** | `{"target_step": "social"}` | Runs the midnight social task and the midnight data indexer. |
23
- | **Global Sync** | `{"target_step": "global"}` | Triggers the `root-data-indexer` with no target date (Full Scan). |
24
- | **Full Pipeline** | `{}` | Runs the entire 24-hour cycle from the beginning (Standard Run). |
14
+ | Phase to Test | JSON Input | Action & Description |
15
+ | :--- | :--- | :--- |
16
+ | **Market Data (Phase 1)** | `{"target_step": "market"}` | **Runs:** Price Fetcher & Insights Fetcher.<br>**Note:** Automatically triggers indexing after each fetch. Workflow will pause at "Wait for Midnight" upon completion. |
17
+ | **Midnight Phase (Phase 3)** | `{"target_step": "midnight"}` | **Runs:** Popular Investor Rankings, Midnight Social Orchestrator, and Global Index Verification.<br>**Note:** Use this to test the critical 00:00 UTC logic without waiting for the daily schedule. |
18
+ | **Social Loop (Phase 4)** | `{"target_step": "social"}` | **Runs:** Enters the recurring 3-hour social fetch loop.<br>**Warning:** Triggers `social_loop_start`, which begins with a 3-hour sleep (`wait_3_hours`) before the first execution. |
19
+ | **Standard Run** | `{}` | **Runs:** The full 24-hour cycle starting from 22:00 UTC (Market Close). |
25
20
 
26
21
  ---
27
22
 
28
- ### ๐Ÿ’ก Pro-Tips for Testing
23
+ ### ๐Ÿ’ก Testing Notes & Pro-Tips
29
24
 
30
- * **Variable Checking:** After an execution finishes, click the **Details** tab in the execution logs to see the final values of variables like `sleep_midnight` or `sleep_loop` to verify the UTC alignment math worked correctly.
31
- * **Permissions:** Ensure your user account or the service account running the workflow has the `roles/workflows.invoker` role to trigger these tests.
32
- * **Timeouts:** If testing the full loop, remember that the workflow will "pause" at the `sys.sleep` steps. You can see the status as **Active** while it waits for the next 3-hour window.
25
+ * **Automatic Indexing:** As of V3.2, you will not see explicit "Index" steps in the workflow visualization for Price, Insights, or Rankings. These functions now trigger the `root-data-indexer` automatically upon completion. The only visible index step is the **Global Verification** in the Midnight Phase.
26
+ * **Verification:** To confirm data was indexed during a test:
27
+ * Check the logs of the individual Cloud Functions (`price-fetcher`, etc.).
28
+ * Or, run the **Midnight Phase** test, which ends with the explicit `global_index_midnight` step.
29
+ * **Variable Checking:** After execution, check the **Variables** tab to view `sleep_midnight` calculations to ensure UTC alignment is functioning correctly.
30
+ * **Permissions:** Ensure the executor has `roles/workflows.invoker`.
@@ -0,0 +1,213 @@
1
+ /**
2
+ * @fileoverview Test Alert Helpers
3
+ * Allows developers to send test alerts for testing the alert system
4
+ */
5
+
6
+ const { FieldValue } = require('@google-cloud/firestore');
7
+ const { getAllAlertTypes, getAlertTypeByComputation } = require('../../../alert-system/helpers/alert_type_registry');
8
+ const { isDeveloperAccount, getDevOverride } = require('./dev_helpers');
9
+
10
+ /**
11
+ * POST /user/dev/test-alert
12
+ * Send a test alert to users
13
+ *
14
+ * Request body:
15
+ * {
16
+ * userCid: number (required) - Developer account CID
17
+ * alertTypeId: string (optional) - Alert type ID, defaults to first available
18
+ * targetUsers: 'all' | 'dev' | number[] (optional) - Who to send to, defaults to 'dev'
19
+ * piCid: number (optional) - PI CID for the alert, defaults to 1
20
+ * piUsername: string (optional) - PI username, defaults to 'TestPI'
21
+ * metadata: object (optional) - Additional metadata for the alert
22
+ * }
23
+ */
24
+ async function sendTestAlert(req, res, dependencies, config) {
25
+ const { db, logger } = dependencies;
26
+ const { userCid, alertTypeId, targetUsers = 'dev', piCid = 1, piUsername = 'TestPI', metadata = {} } = req.body;
27
+
28
+ if (!userCid) {
29
+ return res.status(400).json({ error: "Missing userCid" });
30
+ }
31
+
32
+ // SECURITY CHECK: Only allow developer accounts
33
+ if (!isDeveloperAccount(userCid)) {
34
+ logger.log('WARN', `[sendTestAlert] Unauthorized attempt by user ${userCid}`);
35
+ return res.status(403).json({
36
+ error: "Forbidden",
37
+ message: "Test alerts are only available for authorized developer accounts"
38
+ });
39
+ }
40
+
41
+ try {
42
+ // Get alert type
43
+ let alertType;
44
+ if (alertTypeId) {
45
+ alertType = getAlertTypeByComputation(alertTypeId);
46
+ if (!alertType) {
47
+ const allTypes = getAllAlertTypes();
48
+ alertType = allTypes.find(t => t.id === alertTypeId);
49
+ }
50
+ }
51
+
52
+ // Default to first available alert type if not specified
53
+ if (!alertType) {
54
+ const allTypes = getAllAlertTypes();
55
+ if (allTypes.length === 0) {
56
+ return res.status(400).json({ error: "No alert types available" });
57
+ }
58
+ alertType = allTypes[0];
59
+ logger.log('INFO', `[sendTestAlert] Using default alert type: ${alertType.id}`);
60
+ }
61
+
62
+ // Determine target user CIDs
63
+ let targetCids = [];
64
+
65
+ if (targetUsers === 'all') {
66
+ // Get all users from user_notifications collection
67
+ const notificationsSnapshot = await db.collection('user_notifications').get();
68
+ targetCids = notificationsSnapshot.docs.map(doc => Number(doc.id));
69
+ logger.log('INFO', `[sendTestAlert] Sending to all ${targetCids.length} users`);
70
+ } else if (targetUsers === 'dev') {
71
+ // Get all developer accounts with dev override enabled
72
+ const devOverridesCollection = config.devOverridesCollection || 'dev_overrides';
73
+ const devOverridesSnapshot = await db.collection(devOverridesCollection).get();
74
+
75
+ for (const doc of devOverridesSnapshot.docs) {
76
+ const data = doc.data();
77
+ if (data.enabled === true) {
78
+ targetCids.push(Number(doc.id));
79
+ }
80
+ }
81
+
82
+ // Also include the requesting developer
83
+ if (!targetCids.includes(Number(userCid))) {
84
+ targetCids.push(Number(userCid));
85
+ }
86
+
87
+ logger.log('INFO', `[sendTestAlert] Sending to ${targetCids.length} developer accounts`);
88
+ } else if (Array.isArray(targetUsers)) {
89
+ // Specific user CIDs
90
+ targetCids = targetUsers.map(cid => Number(cid)).filter(cid => !isNaN(cid) && cid > 0);
91
+ logger.log('INFO', `[sendTestAlert] Sending to ${targetCids.length} specific users`);
92
+ } else {
93
+ return res.status(400).json({
94
+ error: "Invalid targetUsers",
95
+ message: "targetUsers must be 'all', 'dev', or an array of user CIDs"
96
+ });
97
+ }
98
+
99
+ if (targetCids.length === 0) {
100
+ return res.status(400).json({
101
+ error: "No target users",
102
+ message: "No users found matching the target criteria"
103
+ });
104
+ }
105
+
106
+ // Generate alert message
107
+ const { generateAlertMessage } = require('../../../alert-system/helpers/alert_type_registry');
108
+ const alertMessage = generateAlertMessage(alertType, piUsername, {
109
+ ...metadata,
110
+ isTest: true,
111
+ testSentBy: Number(userCid),
112
+ testSentAt: new Date().toISOString()
113
+ });
114
+
115
+ // Create notifications for each target user
116
+ const batch = db.batch();
117
+ const notificationRefs = [];
118
+ const counterUpdates = {};
119
+ const today = new Date().toISOString().split('T')[0];
120
+
121
+ for (const targetCid of targetCids) {
122
+ const notificationId = `test_alert_${Date.now()}_${targetCid}_${piCid}_${Math.random().toString(36).substring(2, 9)}`;
123
+ const notificationRef = db.collection('user_notifications')
124
+ .doc(String(targetCid))
125
+ .collection('notifications')
126
+ .doc(notificationId);
127
+
128
+ const notificationData = {
129
+ id: notificationId,
130
+ type: 'alert',
131
+ title: `[TEST] ${alertType.name}`,
132
+ message: alertMessage,
133
+ read: false,
134
+ createdAt: FieldValue.serverTimestamp(),
135
+ metadata: {
136
+ piCid: Number(piCid),
137
+ piUsername: piUsername,
138
+ alertType: alertType.id,
139
+ alertTypeName: alertType.name,
140
+ computationName: alertType.computationName,
141
+ computationDate: today,
142
+ severity: alertType.severity,
143
+ isTest: true,
144
+ testSentBy: Number(userCid),
145
+ testSentAt: new Date().toISOString(),
146
+ ...metadata
147
+ }
148
+ };
149
+
150
+ batch.set(notificationRef, notificationData);
151
+ notificationRefs.push(notificationRef);
152
+
153
+ // Track counter updates
154
+ if (!counterUpdates[targetCid]) {
155
+ counterUpdates[targetCid] = {
156
+ date: today,
157
+ unreadCount: 0,
158
+ totalCount: 0,
159
+ byType: {}
160
+ };
161
+ }
162
+ counterUpdates[targetCid].unreadCount += 1;
163
+ counterUpdates[targetCid].totalCount += 1;
164
+ counterUpdates[targetCid].byType[alertType.id] =
165
+ (counterUpdates[targetCid].byType[alertType.id] || 0) + 1;
166
+ }
167
+
168
+ // Update notification counters
169
+ for (const [targetCid, counter] of Object.entries(counterUpdates)) {
170
+ const counterRef = db.collection('user_notifications')
171
+ .doc(String(targetCid))
172
+ .collection('counters')
173
+ .doc(counter.date);
174
+
175
+ batch.set(counterRef, {
176
+ date: counter.date,
177
+ unreadCount: FieldValue.increment(counter.unreadCount),
178
+ totalCount: FieldValue.increment(counter.totalCount),
179
+ [`byType.${alertType.id}`]: FieldValue.increment(counter.byType[alertType.id] || 0),
180
+ lastUpdated: FieldValue.serverTimestamp()
181
+ }, { merge: true });
182
+ }
183
+
184
+ // Commit batch
185
+ await batch.commit();
186
+
187
+ logger.log('SUCCESS', `[sendTestAlert] Created ${notificationRefs.length} test notifications for alert type ${alertType.id}`);
188
+
189
+ return res.status(200).json({
190
+ success: true,
191
+ message: `Test alert sent to ${targetCids.length} users`,
192
+ alertType: {
193
+ id: alertType.id,
194
+ name: alertType.name,
195
+ computationName: alertType.computationName
196
+ },
197
+ targetUsers: {
198
+ count: targetCids.length,
199
+ cids: targetCids
200
+ },
201
+ piCid: Number(piCid),
202
+ piUsername: piUsername,
203
+ notificationsCreated: notificationRefs.length
204
+ });
205
+
206
+ } catch (error) {
207
+ logger.log('ERROR', `[sendTestAlert] Error sending test alert:`, error);
208
+ return res.status(500).json({ error: error.message });
209
+ }
210
+ }
211
+
212
+ module.exports = { sendTestAlert };
213
+
@@ -12,6 +12,7 @@ const { setDevOverride, getDevOverrideStatus } = require('./helpers/dev_helpers'
12
12
  const { getAlertTypes, getDynamicWatchlistComputations, getUserAlerts, getAlertCount, markAlertRead, markAllAlertsRead, deleteAlert } = require('./helpers/alert_helpers');
13
13
  const { requestPiFetch, getPiFetchStatus } = require('./helpers/on_demand_fetch_helpers');
14
14
  const { requestUserSync, getUserSyncStatus } = require('./helpers/user_sync_helpers');
15
+ const { sendTestAlert } = require('./helpers/test_alert_helpers');
15
16
 
16
17
  module.exports = (dependencies, config) => {
17
18
  const router = express.Router();
@@ -87,6 +88,7 @@ module.exports = (dependencies, config) => {
87
88
  // --- Developer Mode (only for whitelisted developer accounts) ---
88
89
  router.post('/dev/override', (req, res) => setDevOverride(req, res, dependencies, config));
89
90
  router.get('/dev/override', (req, res) => getDevOverrideStatus(req, res, dependencies, config));
91
+ router.post('/dev/test-alert', (req, res) => sendTestAlert(req, res, dependencies, config));
90
92
 
91
93
  // --- Alert Management ---
92
94
  router.get('/me/alert-types', (req, res) => getAlertTypes(req, res, dependencies, config));
@@ -270,19 +270,33 @@ exports.runRootDataIndexer = async (config, dependencies) => {
270
270
  ]);
271
271
 
272
272
  // Check if date exists in tracking documents
273
+ // The _dates document uses dot notation: fetchedDates.2025-12-29: true
274
+ // When read, this becomes: { fetchedDates: { "2025-12-29": true } }
273
275
  let foundPISocial = false;
274
276
  let foundSignedInSocial = false;
275
277
 
276
278
  if (piSocialTrackingDoc.exists) {
277
279
  const data = piSocialTrackingDoc.data();
278
- if (data.fetchedDates && data.fetchedDates[dateStr] === true) {
280
+ // Check both nested structure and flat dot-notation structure
281
+ if (data.fetchedDates && typeof data.fetchedDates === 'object') {
282
+ if (data.fetchedDates[dateStr] === true) {
283
+ foundPISocial = true;
284
+ }
285
+ } else if (data[`fetchedDates.${dateStr}`] === true) {
286
+ // Handle flat dot-notation structure (if Firestore stores it that way)
279
287
  foundPISocial = true;
280
288
  }
281
289
  }
282
290
 
283
291
  if (signedInSocialTrackingDoc.exists) {
284
292
  const data = signedInSocialTrackingDoc.data();
285
- if (data.fetchedDates && data.fetchedDates[dateStr] === true) {
293
+ // Check both nested structure and flat dot-notation structure
294
+ if (data.fetchedDates && typeof data.fetchedDates === 'object') {
295
+ if (data.fetchedDates[dateStr] === true) {
296
+ foundSignedInSocial = true;
297
+ }
298
+ } else if (data[`fetchedDates.${dateStr}`] === true) {
299
+ // Handle flat dot-notation structure (if Firestore stores it that way)
286
300
  foundSignedInSocial = true;
287
301
  }
288
302
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.493",
3
+ "version": "1.0.494",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [