SPB Git

spb/vquant Public MIT

VibeQuant — AI-powered institutional-grade financial intelligence platform.

TypeScript 84.3% Python 11.7% JavaScript 1.6% CSS 1.5% HTML 0.7%
5.1 KB · 146 lines typescript
Raw Blame History
1/*2 * =============================================================================3 *  VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 *  File:      server/routes/analytics.ts6 *7 *  Author:    Simon-Pierre Boucher8 *  Contact:   contact@spboucher.ai9 *  Website:   https://www.spboucher.ai10 *  Demo:      https://www.vquant.ai11 *  License:   MIT (see LICENSE)12 *13 *  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.14 * =============================================================================15 */1617import type { Express, Request, Response } from "express";18import { storage } from "../storage";19import { requireAdmin } from "./admin";20import { logger } from "../utils/logger";21import { heartbeatBodySchema } from "./validation";2223export function registerAnalyticsRoutes(app: Express) {24  // Heartbeat - Update user activity25  app.post("/api/analytics/heartbeat", async (req: Request, res: Response) => {26    try {27      const parsed = heartbeatBodySchema.safeParse(req.body);28      if (!parsed.success) {29        return res.status(400).json({ error: parsed.error.issues[0].message });30      }31      const { sessionId, status, currentQuery, userId } = parsed.data;3233      const userAgent = req.headers['user-agent'] || 'Unknown';34      const ipAddress = req.ip || req.socket.remoteAddress || 'Unknown';3536      await storage.upsertActiveUser({37        sessionId,38        userId: userId || null,39        status: status || 'idle',40        currentQuery: currentQuery || null,41        lastHeartbeat: new Date(),42        userAgent,43        ipAddress,44      });4546      res.json({ success: true });47    } catch (error) {48      logger.error("Heartbeat error:", error);49      res.status(500).json({ error: "Failed to update heartbeat" });50    }51  });5253  // Get active users54  app.get("/api/analytics/active-users", requireAdmin, async (req: Request, res: Response) => {55    try {56      await storage.cleanupStaleUsers(2);57      const activeUsers = await storage.getActiveUsers();5859      const statusCounts = activeUsers.reduce((acc, user) => {60        acc[user.status || 'idle'] = (acc[user.status || 'idle'] || 0) + 1;61        return acc;62      }, {} as Record<string, number>);6364      res.json({65        total: activeUsers.length,66        generating: statusCounts.generating || 0,67        idle: statusCounts.idle || 0,68        error: statusCounts.error || 0,69        users: activeUsers.map(u => ({70          sessionId: u.sessionId,71          userId: u.userId,72          status: u.status,73          currentQuery: u.currentQuery,74          lastHeartbeat: u.lastHeartbeat,75          userAgent: u.userAgent,76        })),77      });78    } catch (error) {79      logger.error("Get active users error:", error);80      res.status(500).json({ error: "Failed to get active users" });81    }82  });8384  // Real-time stats (public)85  app.get("/api/analytics/real-time-stats", async (req: Request, res: Response) => {86    try {87      await storage.cleanupStaleUsers(2);88      const activeUsers = await storage.getActiveUsers();89      const allSessions = await storage.getAllConversationSessions();9091      const totalTokens = allSessions.reduce((sum, s) => sum + (s.inputTokens || 0) + (s.outputTokens || 0), 0);92      const totalCost = allSessions.reduce((sum, s) => sum + (s.totalCost || 0), 0);9394      const recentLogs = await storage.getRecentRequestLogs(100);95      const recentErrors = recentLogs.filter(log => log.status === 'error');9697      res.json({98        activeUsers: {99          total: activeUsers.length,100          generating: activeUsers.filter(u => u.status === 'generating').length,101        },102        sessions: {103          total: allSessions.length,104          today: allSessions.filter(s => {105            const createdAt = new Date(s.createdAt);106            return createdAt.toDateString() === new Date().toDateString();107          }).length,108        },109        tokens: {110          total: totalTokens,111          input: allSessions.reduce((sum, s) => sum + (s.inputTokens || 0), 0),112          output: allSessions.reduce((sum, s) => sum + (s.outputTokens || 0), 0),113        },114        cost: {115          total: totalCost,116          today: allSessions117            .filter(s => new Date(s.createdAt).toDateString() === new Date().toDateString())118            .reduce((sum, s) => sum + (s.totalCost || 0), 0),119        },120        errors: {121          lastHour: recentErrors.length,122          rate: recentLogs.length > 0 ? (recentErrors.length / recentLogs.length) * 100 : 0,123        },124      });125    } catch (error) {126      logger.error("Get real-time stats error:", error);127      res.status(500).json({ error: "Failed to get stats" });128    }129  });130131  // Get analytics metrics132  app.get("/api/analytics/metrics", requireAdmin, async (req: Request, res: Response) => {133    try {134      const { periodType = 'minute', limit = 60 } = req.query;135      const metrics = await storage.getLatestMetrics(136        periodType as string,137        parseInt(limit as string),138      );139      res.json({ metrics });140    } catch (error) {141      logger.error("Get metrics error:", error);142      res.status(500).json({ error: "Failed to get metrics" });143    }144  });145}146