bulltrackers-module 1.0.296 → 1.0.298

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,12 +6,14 @@ const { FieldValue, FieldPath } = require('@google-cloud/firestore');
6
6
  const crypto = require('crypto');
7
7
 
8
8
  // [NEW] Single Source of Truth for Data Availability
9
+ // These are defaults/fallbacks. The dynamic function getEarliestDataDates below is the primary source.
9
10
  const DEFINITIVE_EARLIEST_DATES = {
10
- portfolio: new Date('2025-09-25T00:00:00Z'),
11
- history: new Date('2025-11-05T00:00:00Z'),
12
- social: new Date('2025-10-30T00:00:00Z'),
13
- insights: new Date('2025-08-26T00:00:00Z'),
14
- price: new Date('2025-08-01T00:00:00Z')
11
+ portfolio: new Date('2025-01-01T00:00:00Z'),
12
+ history: new Date('2025-08-01T00:00:00Z'),
13
+ social: new Date('2025-08-01T00:00:00Z'),
14
+ insights: new Date('2025-08-01T00:00:00Z'),
15
+ price: new Date('2025-08-01T00:00:00Z'),
16
+ absoluteEarliest: new Date('2023-08-01T00:00:00Z')
15
17
  };
16
18
 
17
19
  /** Stage 1: Normalize a calculation name to kebab-case */
@@ -162,95 +164,75 @@ function getExpectedDateStrings(startDate, endDate) {
162
164
  return dateStrings;
163
165
  }
164
166
 
165
- /** Stage 4: Get the earliest date in a *flat* collection where doc IDs are dates. */
166
- async function getFirstDateFromSimpleCollection(config, deps, collectionName) {
167
- const { db, logger } = deps;
168
- try {
169
- if (!collectionName) { logger.log('WARN', `[Core Utils] Collection name not provided for simple date query.`); return null; }
170
- const query = db.collection(collectionName).where(FieldPath.documentId(), '>=', '2000-01-01').orderBy(FieldPath.documentId(), 'asc').limit(1);
171
- const snapshot = await withRetry(() => query.get(), `GetEarliestDoc(${collectionName})`);
172
- if (!snapshot.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshot.docs[0].id)) { return new Date(snapshot.docs[0].id + 'T00:00:00Z'); }
173
- } catch (e) { logger.log('ERROR', `GetFirstDate failed for ${collectionName}`, { errorMessage: e.message }); }
174
- return null;
175
- }
176
-
177
- /** Stage 4: Get the earliest date in a sharded collection */
178
- async function getFirstDateFromCollection(config, deps, collectionName) {
167
+ /** * Stage 4: Get the earliest date from the Centralized Root Data Index.
168
+ * This REPLACES the expensive/error-prone raw collection scanning.
169
+ * It queries the 'system_root_data_index' to find the first date where data flags are TRUE.
170
+ */
171
+ async function getEarliestDataDates(config, deps) {
179
172
  const { db, logger } = deps;
180
- let earliestDate = null;
181
- try {
182
- if (!collectionName) { logger.log('WARN', `[Core Utils] Collection name not provided for sharded date query.`); return null; }
183
- const blockDocRefs = await withRetry(() => db.collection(collectionName).listDocuments(), `GetBlocks(${collectionName})`);
184
- if (!blockDocRefs.length) { logger.log('WARN', `No block documents in collection: ${collectionName}`); return null; }
185
- for (const blockDocRef of blockDocRefs) {
186
- const snapshotQuery = blockDocRef.collection(config.snapshotsSubcollection).where(FieldPath.documentId(), '>=', '2000-01-01').orderBy(FieldPath.documentId(), 'asc').limit(1);
187
- const snapshotSnap = await withRetry(() => snapshotQuery.get(), `GetEarliestSnapshot(${blockDocRef.path})`);
188
- if (!snapshotSnap.empty && /^\d{4}-\d{2}-\d{2}$/.test(snapshotSnap.docs[0].id)) {
189
- const foundDate = new Date(snapshotSnap.docs[0].id + 'T00:00:00Z');
190
- if (!earliestDate || foundDate < earliestDate) earliestDate = foundDate;
173
+ // Default to 'system_root_data_index' if not configured
174
+ const indexCollection = process.env.ROOT_DATA_AVAILABILITY_COLLECTION ||
175
+ config.rootDataAvailabilityCollection ||
176
+ 'system_root_data_index';
177
+
178
+ // Helper to find earliest date where a specific flag is true
179
+ // Efficient: Uses the document ID (date string) index.
180
+ const getEarliestForType = async (flagName) => {
181
+ try {
182
+ const snapshot = await db.collection(indexCollection)
183
+ .where(flagName, '==', true)
184
+ .orderBy(FieldPath.documentId(), 'asc')
185
+ .limit(1)
186
+ .get();
187
+
188
+ if (!snapshot.empty) {
189
+ const dateStr = snapshot.docs[0].id; // YYYY-MM-DD
190
+ // Safety check on date format
191
+ if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
192
+ return new Date(dateStr + 'T00:00:00Z');
193
+ }
191
194
  }
195
+ } catch (e) {
196
+ logger.log('WARN', `[Utils] Failed to query index for ${flagName}: ${e.message}`);
192
197
  }
193
- } catch (e) { logger.log('ERROR', `GetFirstDate failed for ${collectionName}`, { errorMessage: e.message }); }
194
- return earliestDate;
195
- }
198
+ return null;
199
+ };
196
200
 
197
- /** Stage 5: Determine the earliest date from *all* source data. */
198
- async function getEarliestDataDates(config, deps) {
199
- const { logger } = deps;
200
- const [ investorDate, speculatorDate, investorHistoryDate, speculatorHistoryDate, insightsDate, socialDate, priceDate ] = await Promise.all([
201
- getFirstDateFromCollection (config, deps, config.normalUserPortfolioCollection),
202
- getFirstDateFromCollection (config, deps, config.speculatorPortfolioCollection),
203
- getFirstDateFromCollection (config, deps, config.normalUserHistoryCollection),
204
- getFirstDateFromCollection (config, deps, config.speculatorHistoryCollection),
205
- getFirstDateFromSimpleCollection (config, deps, config.insightsCollectionName),
206
- getFirstDateFromSimpleCollection (config, deps, config.socialInsightsCollectionName),
207
- getFirstDateFromPriceCollection (config, deps)
201
+ // Parallel query for all data types
202
+ const [portfolioDate, historyDate, socialDate, insightsDate, priceDate] = await Promise.all([
203
+ getEarliestForType('hasPortfolio'),
204
+ getEarliestForType('hasHistory'),
205
+ getEarliestForType('hasSocial'),
206
+ getEarliestForType('hasInsights'),
207
+ getEarliestForType('hasPrices')
208
208
  ]);
209
209
 
210
- const getMinDate = (...dates) => {
211
- const validDates = dates.filter(Boolean);
212
- if (validDates.length === 0) return null;
213
- return new Date(Math.min(...validDates));
214
- };
215
-
216
- const earliestPortfolioDate = getMinDate(investorDate, speculatorDate);
217
- const earliestHistoryDate = getMinDate(investorHistoryDate, speculatorHistoryDate);
218
- const earliestInsightsDate = getMinDate(insightsDate);
219
- const earliestSocialDate = getMinDate(socialDate);
220
- const earliestPriceDate = getMinDate(priceDate);
221
- const absoluteEarliest = getMinDate(earliestPortfolioDate, earliestHistoryDate, earliestInsightsDate, earliestSocialDate, earliestPriceDate);
210
+ // Calculate absolute earliest among found dates
211
+ const foundDates = [portfolioDate, historyDate, socialDate, insightsDate, priceDate].filter(d => d !== null);
212
+
213
+ let absoluteEarliest = null;
214
+ if (foundDates.length > 0) {
215
+ absoluteEarliest = new Date(Math.min(...foundDates));
216
+ } else {
217
+ // Fallback if index is empty
218
+ const configStart = config.earliestComputationDate || '2023-01-01';
219
+ absoluteEarliest = new Date(configStart + 'T00:00:00Z');
220
+ logger.log('WARN', `[Utils] No data found in Root Data Index (${indexCollection}). Defaulting to ${configStart}`);
221
+ }
222
222
 
223
- const fallbackDate = new Date(config.earliestComputationDate + 'T00:00:00Z' || '2023-01-01T00:00:00Z');
223
+ // Update the static export for consumers who use it (best effort synchronization)
224
+ DEFINITIVE_EARLIEST_DATES.absoluteEarliest = absoluteEarliest;
224
225
 
225
226
  return {
226
- portfolio: earliestPortfolioDate || new Date('2999-12-31T00:00:00Z'),
227
- history: earliestHistoryDate || new Date('2999-12-31T00:00:00Z'),
228
- insights: earliestInsightsDate || new Date('2999-12-31T00:00:00Z'),
229
- social: earliestSocialDate || new Date('2999-12-31T00:00:00Z'),
230
- price: earliestPriceDate || new Date('2999-12-31T00:00:00Z'),
231
- absoluteEarliest: absoluteEarliest || fallbackDate
227
+ portfolio: portfolioDate || new Date('2999-12-31T00:00:00Z'),
228
+ history: historyDate || new Date('2999-12-31T00:00:00Z'),
229
+ insights: insightsDate || new Date('2999-12-31T00:00:00Z'),
230
+ social: socialDate || new Date('2999-12-31T00:00:00Z'),
231
+ price: priceDate || new Date('2999-12-31T00:00:00Z'),
232
+ absoluteEarliest: absoluteEarliest
232
233
  };
233
234
  }
234
235
 
235
- async function getFirstDateFromPriceCollection(config, deps) {
236
- const { db, logger } = deps;
237
- const collection = config.priceCollection || 'asset_prices';
238
- try {
239
- const snapshot = await withRetry(() => db.collection(collection).limit(10).get(), `GetPriceShards(${collection})`);
240
- let earliestDate = null;
241
- snapshot.forEach(doc => {
242
- const shardData = doc.data();
243
- for (const instrumentId in shardData) {
244
- const instrumentData = shardData[instrumentId];
245
- if (!instrumentData.prices) continue;
246
- const dates = Object.keys(instrumentData.prices).filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d)).sort();
247
- if (dates.length > 0) { const firstDate = new Date(dates[0] + 'T00:00:00Z'); if (!earliestDate || firstDate < earliestDate) earliestDate = firstDate; }
248
- }
249
- });
250
- return earliestDate;
251
- } catch (e) { logger.log('ERROR', `Failed to get earliest price date from ${collection}`, { errorMessage: e.message }); return null; }
252
- }
253
-
254
236
  module.exports = {
255
237
  FieldValue,
256
238
  FieldPath,
@@ -259,7 +241,7 @@ module.exports = {
259
241
  getExpectedDateStrings,
260
242
  getEarliestDataDates,
261
243
  generateCodeHash,
262
- generateDataHash, // Exported
244
+ generateDataHash,
263
245
  withRetry,
264
246
  DEFINITIVE_EARLIEST_DATES
265
- };
247
+ };
@@ -2,6 +2,7 @@
2
2
  * @fileoverview Admin API Router
3
3
  * Sub-module for system observability, debugging, and visualization.
4
4
  * Mounted at /admin within the Generic API.
5
+ * UPDATED: Added advanced cost, performance, and live monitoring endpoints.
5
6
  */
6
7
 
7
8
  const express = require('express');
@@ -84,8 +85,6 @@ const createAdminRouter = (config, dependencies, unifiedCalculations) => {
84
85
  });
85
86
 
86
87
  // --- 2. STATUS MATRIX (Calendar / State UI) ---
87
- // Returns status of ALL computations across a date range.
88
- // ENHANCED: Cross-references Manifest to detect "PENDING" (Not run yet) vs "MISSING".
89
88
  router.get('/matrix', async (req, res) => {
90
89
  const { start, end } = req.query;
91
90
  if (!start || !end) return res.status(400).json({ error: "Start and End dates required." });
@@ -153,7 +152,6 @@ const createAdminRouter = (config, dependencies, unifiedCalculations) => {
153
152
  });
154
153
 
155
154
  // --- 3. PIPELINE STATE (Progress Bar) ---
156
- // Shows realtime status of the 5-pass system for a specific date
157
155
  router.get('/pipeline/state', async (req, res) => {
158
156
  const { date } = req.query;
159
157
  if (!date) return res.status(400).json({ error: "Date required" });
@@ -352,7 +350,6 @@ const createAdminRouter = (config, dependencies, unifiedCalculations) => {
352
350
  });
353
351
 
354
352
  // --- 7. FLIGHT RECORDER (Inspection) ---
355
- // Existing inspection endpoint kept for drill-down
356
353
  router.get('/inspect/:date/:calcName', async (req, res) => {
357
354
  const { date, calcName } = req.params;
358
355
  try {
@@ -379,6 +376,156 @@ const createAdminRouter = (config, dependencies, unifiedCalculations) => {
379
376
  }
380
377
  });
381
378
 
379
+ // --- 8. COST & RESOURCE ANALYSIS ---
380
+ router.get('/analytics/costs', async (req, res) => {
381
+ const { date, days } = req.query;
382
+ // Default to today if no date, or range if days provided
383
+ const targetDate = date || new Date().toISOString().slice(0, 10);
384
+
385
+ // Simple Cost Model (Estimates)
386
+ const COSTS = {
387
+ write: 0.18 / 100000,
388
+ read: 0.06 / 100000,
389
+ delete: 0.02 / 100000,
390
+ compute_std_sec: 0.000023, // 1vCPU 2GB (approx)
391
+ compute_high_sec: 0.000092 // 2vCPU 8GB (approx)
392
+ };
393
+
394
+ try {
395
+ const auditRef = db.collection('computation_audit_logs');
396
+ // We scan the 'history' subcollectionGroup for the given date(s)
397
+ // Note: This can be expensive. In prod, you'd want aggregate counters.
398
+ const query = db.collectionGroup('history').where('targetDate', '==', targetDate);
399
+ const snap = await query.get();
400
+
401
+ let totalCost = 0;
402
+ const byPass = {};
403
+ const byCalc = {};
404
+
405
+ snap.forEach(doc => {
406
+ const data = doc.data();
407
+ const ops = data.firestoreOps || { reads: 0, writes: 0, deletes: 0 };
408
+ const durationSec = (data.durationMs || 0) / 1000;
409
+ const tier = data.resourceTier || 'standard';
410
+
411
+ const ioCost = (ops.writes * COSTS.write) + (ops.reads * COSTS.read) + (ops.deletes * COSTS.delete);
412
+ const computeCost = durationSec * (tier === 'high-mem' ? COSTS.compute_high_sec : COSTS.compute_std_sec);
413
+ const itemCost = ioCost + computeCost;
414
+
415
+ totalCost += itemCost;
416
+
417
+ // Aggregations
418
+ const pass = data.pass || 'unknown';
419
+ if (!byPass[pass]) byPass[pass] = { cost: 0, runs: 0, duration: 0 };
420
+ byPass[pass].cost += itemCost;
421
+ byPass[pass].runs++;
422
+ byPass[pass].duration += durationSec;
423
+
424
+ const calc = data.computationName;
425
+ if (!byCalc[calc]) byCalc[calc] = { cost: 0, runs: 0, ops: { r:0, w:0 } };
426
+ byCalc[calc].cost += itemCost;
427
+ byCalc[calc].runs++;
428
+ byCalc[calc].ops.r += ops.reads;
429
+ byCalc[calc].ops.w += ops.writes;
430
+ });
431
+
432
+ // Top 10 Expensive Calcs
433
+ const topCalcs = Object.entries(byCalc)
434
+ .sort((a, b) => b[1].cost - a[1].cost)
435
+ .slice(0, 10)
436
+ .map(([name, stats]) => ({ name, ...stats }));
437
+
438
+ res.json({
439
+ date: targetDate,
440
+ totalCostUSD: totalCost,
441
+ breakdown: {
442
+ byPass,
443
+ topCalculations: topCalcs
444
+ },
445
+ meta: { model: COSTS }
446
+ });
447
+
448
+ } catch (e) { res.status(500).json({ error: e.message }); }
449
+ });
450
+
451
+ // --- 9. REROUTE (OOM) ANALYSIS ---
452
+ router.get('/analytics/reroutes', async (req, res) => {
453
+ const { date } = req.query;
454
+ if (!date) return res.status(400).json({ error: "Date required" });
455
+
456
+ try {
457
+ // Find all runs that used high-mem
458
+ const query = db.collectionGroup('history')
459
+ .where('targetDate', '==', date)
460
+ .where('resourceTier', '==', 'high-mem');
461
+
462
+ const snap = await query.get();
463
+ const reroutes = [];
464
+
465
+ snap.forEach(doc => {
466
+ const data = doc.data();
467
+ reroutes.push({
468
+ computation: data.computationName,
469
+ pass: data.pass,
470
+ trigger: data.trigger?.reason,
471
+ peakMemoryMB: data.peakMemoryMB,
472
+ durationMs: data.durationMs,
473
+ runId: data.runId
474
+ });
475
+ });
476
+
477
+ res.json({ count: reroutes.length, reroutes });
478
+ } catch (e) { res.status(500).json({ error: e.message }); }
479
+ });
480
+
481
+ // --- 10. LIVE DASHBOARD (Snapshot) ---
482
+ // Poll this endpoint to simulate a WebSocket feed
483
+ router.get('/live/dashboard', async (req, res) => {
484
+ const today = new Date().toISOString().slice(0, 10);
485
+ try {
486
+ // Query the Ledger for Active Tasks
487
+ // We look at all passes for today
488
+ const passes = ['1', '2', '3', '4', '5'];
489
+ const activeTasks = [];
490
+ const recentFailures = [];
491
+
492
+ await Promise.all(passes.map(async (pass) => {
493
+ const colRef = db.collection(`computation_audit_ledger/${today}/passes/${pass}/tasks`);
494
+
495
+ // Get Running
496
+ const runningSnap = await colRef.where('status', 'in', ['PENDING', 'IN_PROGRESS']).get();
497
+ runningSnap.forEach(doc => {
498
+ activeTasks.push({ pass, ...doc.data() });
499
+ });
500
+
501
+ // Get Recent Failures (last 10 mins?? hard to query without index, just grab failures)
502
+ const failSnap = await colRef.where('status', '==', 'FAILED').get();
503
+ failSnap.forEach(doc => {
504
+ recentFailures.push({ pass, ...doc.data() });
505
+ });
506
+ }));
507
+
508
+ // Get Pipeline Stage (which pass is active?)
509
+ // We infer this by seeing which pass has pending tasks
510
+ let currentStage = 'IDLE';
511
+ for (const p of passes) {
512
+ const hasActive = activeTasks.some(t => t.pass === p);
513
+ if (hasActive) { currentStage = `PASS_${p}`; break; }
514
+ }
515
+
516
+ res.json({
517
+ status: 'success',
518
+ timestamp: new Date(),
519
+ pipelineState: currentStage,
520
+ activeCount: activeTasks.length,
521
+ failureCount: recentFailures.length,
522
+ tasks: activeTasks,
523
+ failures: recentFailures
524
+ });
525
+
526
+ } catch (e) { res.status(500).json({ error: e.message }); }
527
+ });
528
+
382
529
  return router;
383
530
  };
384
531
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.296",
3
+ "version": "1.0.298",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [