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%
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