Merge branch 'fix/lint-issues'
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { node: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: {
|
||||
// Allow unused vars that start with underscore
|
||||
'@typescript-eslint/no-unused-vars': ['error', {
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
destructuredArrayIgnorePattern: '^_',
|
||||
}],
|
||||
// Allow any types (common in API/external data handling)
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
}
|
||||
@@ -118,7 +118,7 @@ function validateConfig(): void {
|
||||
required.push(['USER_DB_PG_URL or TSDB_PG_URL', config.userDb.url]);
|
||||
}
|
||||
|
||||
const missing = required.filter(([name, value]) => !value);
|
||||
const missing = required.filter(([, value]) => !value);
|
||||
|
||||
if (missing.length > 0) {
|
||||
const names = missing.map(([name]) => name).join(', ');
|
||||
|
||||
@@ -82,8 +82,8 @@ router.get('/get-current-team', async (req: Request, res: Response) => {
|
||||
teamId: team.id,
|
||||
teamName: team.name,
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error('[IAMController] /get-current-team error:', err.message);
|
||||
} catch (err) {
|
||||
console.error('[IAMController] /get-current-team error:', err instanceof Error ? err.message : err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
msg: 'Failed to get current team',
|
||||
@@ -142,8 +142,8 @@ router.get('/team/get-team-role-by-id/:teamId', async (req: Request, res: Respon
|
||||
const roleId = membership ? (roleMap[membership.role] || 2) : 2;
|
||||
|
||||
res.json({ roleId });
|
||||
} catch (err: any) {
|
||||
console.error('[IAMController] /team/get-team-role-by-id error:', err.message);
|
||||
} catch (err) {
|
||||
console.error('[IAMController] /team/get-team-role-by-id error:', err instanceof Error ? err.message : err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
msg: 'Failed to get team role',
|
||||
|
||||
@@ -17,15 +17,6 @@ const router = express.Router();
|
||||
|
||||
const AUTH_MIDDLEWARE = passport.authenticate("jwt", { session: false });
|
||||
|
||||
const isRecord = (v: unknown): v is Record<string, unknown> => {
|
||||
return typeof v === "object" && v !== null && !Array.isArray(v);
|
||||
};
|
||||
|
||||
const utcDateKey = (d: Date): string => {
|
||||
const x = new Date(d);
|
||||
x.setUTCHours(0, 0, 0, 0);
|
||||
return x.toISOString().slice(0, 10);
|
||||
};
|
||||
|
||||
interface TokenContext {
|
||||
team_id: string;
|
||||
@@ -318,7 +309,7 @@ router.get("/logs", AUTH_MIDDLEWARE, async (req: Request, res: Response) => {
|
||||
|
||||
// Convert to array and sort by cost desc
|
||||
return Array.from(merged.values())
|
||||
.map(({ latency_sum, ...rest }) => ({ ...rest, latency_sum: 0 }))
|
||||
.map(({ latency_sum: _latency_sum, ...rest }) => ({ ...rest, latency_sum: 0 }))
|
||||
.sort((a, b) => b.total_cost - a.total_cost);
|
||||
};
|
||||
|
||||
|
||||
@@ -35,9 +35,10 @@ const EMAIL_REGEX =
|
||||
*/
|
||||
router.post(
|
||||
"/login-v2",
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
async (req: Request, res: Response, _next: NextFunction) => {
|
||||
try {
|
||||
let { email, password } = req.body;
|
||||
let { email } = req.body;
|
||||
const { password } = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (
|
||||
@@ -102,32 +103,29 @@ router.post(
|
||||
current_team_id: result.current_team_id,
|
||||
create_time: result.created_at,
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("[UserController] login-v2 error:", err.message);
|
||||
} catch (err) {
|
||||
const error = err as { message?: string; code?: string };
|
||||
console.error("[UserController] login-v2 error:", error.message);
|
||||
|
||||
// Handle specific error codes
|
||||
if (err.code === "USER_NOT_FOUND") {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
msg: "User not found. Please sign up for an account.",
|
||||
});
|
||||
}
|
||||
|
||||
if (err.code === "INVALID_CREDENTIALS") {
|
||||
if (
|
||||
error.code === "USER_NOT_FOUND" ||
|
||||
error.code === "INVALID_CREDENTIALS"
|
||||
) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
msg: "Invalid email or password",
|
||||
});
|
||||
}
|
||||
|
||||
if (err.code === "OAUTH_REQUIRED") {
|
||||
if (error.code === "OAUTH_REQUIRED") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
msg: err.message,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (err.code === "ACCOUNT_DISABLED") {
|
||||
if (error.code === "ACCOUNT_DISABLED") {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
msg: "Your account has been disabled",
|
||||
@@ -151,7 +149,8 @@ router.post(
|
||||
*/
|
||||
router.post("/register", async (req: Request, res: Response) => {
|
||||
try {
|
||||
let { email, password, name, firstname, lastname } = req.body;
|
||||
let { email } = req.body;
|
||||
const { password, name, firstname, lastname } = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (
|
||||
@@ -479,7 +478,7 @@ router.post("/generate-dev-token", async (req: Request, res: Response) => {
|
||||
*/
|
||||
const DEFAULT_UI_SETTINGS = {
|
||||
sidebarCollapsed: false,
|
||||
performanceDashboardTimeRange: 'today',
|
||||
performanceDashboardTimeRange: "today",
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -511,8 +510,11 @@ router.get("/settings", async (req: Request, res: Response) => {
|
||||
// Extract UI settings from preferences, merge with defaults
|
||||
const preferences = user.preferences || {};
|
||||
const uiSettings = {
|
||||
sidebarCollapsed: preferences.sidebarCollapsed ?? DEFAULT_UI_SETTINGS.sidebarCollapsed,
|
||||
performanceDashboardTimeRange: preferences.performanceDashboardTimeRange ?? DEFAULT_UI_SETTINGS.performanceDashboardTimeRange,
|
||||
sidebarCollapsed:
|
||||
preferences.sidebarCollapsed ?? DEFAULT_UI_SETTINGS.sidebarCollapsed,
|
||||
performanceDashboardTimeRange:
|
||||
preferences.performanceDashboardTimeRange ??
|
||||
DEFAULT_UI_SETTINGS.performanceDashboardTimeRange,
|
||||
};
|
||||
|
||||
res.json({
|
||||
@@ -558,7 +560,7 @@ router.put("/settings", async (req: Request, res: Response) => {
|
||||
|
||||
// Build update object with only provided fields
|
||||
const updates: Record<string, any> = {};
|
||||
if (typeof sidebarCollapsed === 'boolean') {
|
||||
if (typeof sidebarCollapsed === "boolean") {
|
||||
updates.sidebarCollapsed = sidebarCollapsed;
|
||||
}
|
||||
if (performanceDashboardTimeRange !== undefined) {
|
||||
@@ -576,23 +578,28 @@ router.put("/settings", async (req: Request, res: Response) => {
|
||||
if (pgPool) {
|
||||
// PostgreSQL - use JSONB
|
||||
await pgPool.query(
|
||||
'UPDATE users SET preferences = $1, updated_at = NOW() WHERE id = $2',
|
||||
"UPDATE users SET preferences = $1, updated_at = NOW() WHERE id = $2",
|
||||
[JSON.stringify(newPreferences), user.id]
|
||||
);
|
||||
} else if (mysqlPool) {
|
||||
// MySQL - use JSON column
|
||||
await mysqlPool.query(
|
||||
'UPDATE user SET preferences = ?, updated_at = NOW() WHERE id = ?',
|
||||
"UPDATE user SET preferences = ?, updated_at = NOW() WHERE id = ?",
|
||||
[JSON.stringify(newPreferences), user.id]
|
||||
);
|
||||
} else {
|
||||
console.warn("[UserController] PUT /settings: No database pool available, settings not persisted");
|
||||
console.warn(
|
||||
"[UserController] PUT /settings: No database pool available, settings not persisted"
|
||||
);
|
||||
}
|
||||
|
||||
// Return updated settings
|
||||
const uiSettings = {
|
||||
sidebarCollapsed: newPreferences.sidebarCollapsed ?? DEFAULT_UI_SETTINGS.sidebarCollapsed,
|
||||
performanceDashboardTimeRange: newPreferences.performanceDashboardTimeRange ?? DEFAULT_UI_SETTINGS.performanceDashboardTimeRange,
|
||||
sidebarCollapsed:
|
||||
newPreferences.sidebarCollapsed ?? DEFAULT_UI_SETTINGS.sidebarCollapsed,
|
||||
performanceDashboardTimeRange:
|
||||
newPreferences.performanceDashboardTimeRange ??
|
||||
DEFAULT_UI_SETTINGS.performanceDashboardTimeRange,
|
||||
};
|
||||
|
||||
res.json({
|
||||
|
||||
@@ -15,9 +15,13 @@ import { initializeSockets, setUserDbService } from "./sockets/control.socket";
|
||||
const PORT = process.env.PORT || 4000;
|
||||
|
||||
// Declare globals for MongoDB (used by services)
|
||||
// eslint-disable-next-line no-var
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var _ACHO_MG_DB: MongoClient;
|
||||
// eslint-disable-next-line no-var
|
||||
var _ACHO_MDB_CONFIG: { ERP_DBNAME: string; DBNAME: string };
|
||||
// eslint-disable-next-line no-var
|
||||
var _ACHO_MDB_COLLECTIONS: {
|
||||
ADEN_CONTROL_POLICIES: string;
|
||||
ADEN_CONTROL_CONTENT: string;
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
createSuccessResponse,
|
||||
handleToolError,
|
||||
} from "../utils/response-helpers";
|
||||
import { idSchema, paginationSchema } from "../utils/schema-helpers";
|
||||
|
||||
export function registerPolicyTools(server: McpServer, api: ApiClient) {
|
||||
// ==================== hive_policies_list ====================
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* - GET /mcp - SSE stream for server-to-client messages
|
||||
* - POST /mcp/message - Client-to-server messages
|
||||
*/
|
||||
import express, { Request, Response, NextFunction, Router } from "express";
|
||||
import express, { Request, Response, Router } from "express";
|
||||
import passport from "passport";
|
||||
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import { createHiveMcpServer, type HiveMcpServerOptions } from "../server";
|
||||
@@ -155,7 +155,7 @@ export function createMcpRouter(
|
||||
|
||||
// Only show sessions for the requesting team
|
||||
const teamSessions = Array.from(sessions.entries())
|
||||
.filter(([_, session]) => session.teamId === teamId)
|
||||
.filter(([, session]) => session.teamId === teamId)
|
||||
.map(([id, session]) => ({
|
||||
session_id: id,
|
||||
team_id: session.teamId,
|
||||
|
||||
@@ -18,7 +18,8 @@ interface HttpError extends Error {
|
||||
* @param {Object} res - Express response
|
||||
* @param {Function} next - Next middleware
|
||||
*/
|
||||
function errorHandler(err: HttpError, req: Request, res: Response, next: NextFunction): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function errorHandler(err: HttpError, req: Request, res: Response, _next: NextFunction): void {
|
||||
// Log error
|
||||
console.error('[Error]', {
|
||||
message: err.message,
|
||||
|
||||
@@ -253,7 +253,7 @@ async function calculateBudgetAnalyticsFromTsdb(teamId: string | number, budget:
|
||||
|
||||
const conditions = [`team_id = $1`, `"timestamp" >= $2`, `"timestamp" <= $3`];
|
||||
const values: unknown[] = [String(teamId), baseTableStart, now];
|
||||
let paramIndex = 4;
|
||||
const paramIndex = 4;
|
||||
|
||||
// Apply budget-specific filter based on budget type
|
||||
const budgetFilter = getBudgetFilter(budget, paramIndex);
|
||||
@@ -663,11 +663,12 @@ function matchesBudgetType(budget: Budget, metricData: MetricData): boolean {
|
||||
// Feature budgets apply when feature name matches
|
||||
return !!metadata.feature && budget.name === metadata.feature;
|
||||
|
||||
case "tag":
|
||||
case "tag": {
|
||||
// Tag budgets apply when the tagCategory value matches budget name
|
||||
if (!budget.tagCategory || !metadata.tags) return false;
|
||||
const tagValue = (metadata.tags as Record<string, string>)[budget.tagCategory];
|
||||
return !!tagValue && budget.name === tagValue;
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
@@ -726,18 +727,21 @@ async function sendBudgetNotifications(budget: Budget, alertData: Record<string,
|
||||
: "#eff6ff";
|
||||
|
||||
// Build notification content
|
||||
let subject: string, title: string, description: string;
|
||||
let title: string, description: string;
|
||||
if (isLimitAction) {
|
||||
subject = `[Aden] Budget "${budget.name}" - ${(action || "").toUpperCase()}`;
|
||||
title = "Budget Limit Triggered";
|
||||
description = `The budget <strong>${budget.name}</strong> has exceeded its limit and triggered a control action.`;
|
||||
} else {
|
||||
subject = `[Aden] Budget "${budget.name}" at ${spentPercentage}%`;
|
||||
title = "Budget Threshold Alert";
|
||||
description = `The budget <strong>${budget.name}</strong> has reached ${threshold}% of its limit.`;
|
||||
}
|
||||
|
||||
const htmlContent = `
|
||||
// Email subject and content prepared for future email notification implementation
|
||||
const _subject = isLimitAction
|
||||
? `[Aden] Budget "${budget.name}" - ${(action || "").toUpperCase()}`
|
||||
: `[Aden] Budget "${budget.name}" at ${spentPercentage}%`;
|
||||
|
||||
const _htmlContent = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -1743,8 +1747,6 @@ async function getBudgetDetails(teamId: string | number, policyId: string | null
|
||||
// Get real-time tracker status
|
||||
const tracker = budgetTracker.get(budgetId);
|
||||
const spent = tracker?.spent ?? budget.spent ?? 0;
|
||||
const remaining = Math.max(0, budget.limit - spent);
|
||||
const usagePercent = budget.limit > 0 ? (spent / budget.limit) * 100 : 0;
|
||||
|
||||
return {
|
||||
...budget,
|
||||
|
||||
@@ -552,7 +552,7 @@ function initAdenControlSockets(io: Server, rootEmitter: RedisEmitter): ControlE
|
||||
}
|
||||
break;
|
||||
|
||||
case "get_policy":
|
||||
case "get_policy": {
|
||||
// Request for current policy
|
||||
const policy = await controlService.getPolicy(teamId!, policyId || null);
|
||||
socket.emit("message", {
|
||||
@@ -560,6 +560,7 @@ function initAdenControlSockets(io: Server, rootEmitter: RedisEmitter): ControlE
|
||||
policy,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn(
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
*/
|
||||
|
||||
// In-memory cache for pricing data
|
||||
let pricingCache = new Map<string, PricingEntry>();
|
||||
let aliasCacheMap = new Map<string, string>(); // model alias -> canonical model
|
||||
const pricingCache = new Map<string, PricingEntry>();
|
||||
const aliasCacheMap = new Map<string, string>(); // model alias -> canonical model
|
||||
let cacheLoadedAt: number | null = null;
|
||||
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
@@ -389,7 +389,7 @@ interface CostResult {
|
||||
* @param {Object} params - Request parameters
|
||||
* @returns {Object} Cost breakdown { total, input_cost, output_cost, cached_cost, pricing }
|
||||
*/
|
||||
function calculateCostSync({ model, provider, input_tokens = 0, output_tokens = 0, cached_tokens = 0 }: CostCalculationParams): CostResult {
|
||||
function calculateCostSync({ model, provider: _provider, input_tokens = 0, output_tokens = 0, cached_tokens = 0 }: CostCalculationParams): CostResult {
|
||||
const resolved = resolveAlias(model);
|
||||
let pricing: { input: number; output: number; cached_input: number; model: string; source: string };
|
||||
|
||||
@@ -577,7 +577,7 @@ async function getAllPricing(): Promise<AllPricingResult> {
|
||||
await loadPricingFromDb();
|
||||
|
||||
const result: AllPricingResult = {};
|
||||
for (const [key, pricing] of pricingCache.entries()) {
|
||||
for (const [, pricing] of pricingCache.entries()) {
|
||||
result[pricing.model] = {
|
||||
provider: pricing.provider,
|
||||
input: pricing.input,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -24,6 +24,21 @@ import {
|
||||
} from './charts/specs'
|
||||
import type { RawJsonData, KPIValues } from '@/types/agentControl'
|
||||
|
||||
// Shape of analytics API response for type safety
|
||||
interface AnalyticsResponse extends RawJsonData {
|
||||
analytics?: {
|
||||
summary?: {
|
||||
total_cost?: number
|
||||
total_requests?: number
|
||||
total_tokens?: number
|
||||
avg_latency_ms?: number
|
||||
cache_savings?: number
|
||||
}
|
||||
}
|
||||
kpis?: Record<string, unknown>
|
||||
summary?: Record<string, unknown>
|
||||
}
|
||||
|
||||
const timeRangeOptions: { value: TimeRange; label: string }[] = [
|
||||
{ value: 'all', label: 'All Time' },
|
||||
{ value: 'month', label: 'Last Month' },
|
||||
@@ -47,7 +62,7 @@ function extractKpis(data: RawJsonData | undefined): KPIValues {
|
||||
if (!data) return defaults
|
||||
|
||||
// Handle new analytics response shape
|
||||
const analyticsData = data as any
|
||||
const analyticsData = data as AnalyticsResponse
|
||||
if (analyticsData?.analytics?.summary) {
|
||||
const summary = analyticsData.analytics.summary
|
||||
return {
|
||||
|
||||
@@ -63,8 +63,8 @@ export function WorkersPanel() {
|
||||
const realtimeAgents = useMemo(() => deriveWorkersFromEvents(eventsBuffer), [eventsBuffer])
|
||||
|
||||
// Merge API agents with real-time updates (real-time overrides API data)
|
||||
const apiAgents = agentsData?.agents || []
|
||||
const workers = useMemo(() => {
|
||||
const apiAgents = agentsData?.agents || []
|
||||
const agentMap = new Map<string, AgentInfo>()
|
||||
// Add API agents first
|
||||
for (const agent of apiAgents) {
|
||||
@@ -75,7 +75,7 @@ export function WorkersPanel() {
|
||||
agentMap.set(agent.agent, agent)
|
||||
}
|
||||
return Array.from(agentMap.values())
|
||||
}, [apiAgents, realtimeAgents])
|
||||
}, [agentsData?.agents, realtimeAgents])
|
||||
|
||||
// Compute summary stats
|
||||
const onlineCount = workers.filter((w: AgentInfo) => w.status === 'connected').length
|
||||
|
||||
@@ -66,10 +66,10 @@ export function CostByModelChart({
|
||||
dataKey="cost"
|
||||
nameKey="name"
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
{data.map((item, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={entry.color || COLORS[index % COLORS.length]}
|
||||
fill={item.color || COLORS[index % COLORS.length]}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
@@ -89,7 +89,7 @@ export function CostByModelChart({
|
||||
maxWidth: '45%',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
formatter={(value, entry) => {
|
||||
formatter={(value) => {
|
||||
const item = data.find((d) => d.name === value)
|
||||
return (
|
||||
<span className="text-sm block truncate max-w-[120px]" title={String(value)}>
|
||||
|
||||
@@ -35,7 +35,6 @@ export function TopAgentsChart({
|
||||
|
||||
// Sort by spend descending and take top 10
|
||||
const sortedData = [...data].sort((a, b) => b.spend - a.spend).slice(0, 10)
|
||||
const maxSpend = Math.max(...sortedData.map((d) => d.spend))
|
||||
|
||||
return (
|
||||
<Card className={className}>
|
||||
|
||||
@@ -6,6 +6,82 @@
|
||||
// Color palette for models (matches launchpad)
|
||||
export const MODEL_COLORS = ['#263A99', '#22c55e', '#6b21a8', '#f59e0b', '#c1392b', '#06b6d4']
|
||||
|
||||
// =============================================================================
|
||||
// API Response Types (for type safety with unknown API data)
|
||||
// =============================================================================
|
||||
|
||||
interface TimelineCostItem {
|
||||
bucket: string
|
||||
cost_total?: number
|
||||
}
|
||||
|
||||
interface TimelineRequestItem {
|
||||
bucket: string
|
||||
requests?: number
|
||||
}
|
||||
|
||||
interface TimelineTokenItem {
|
||||
bucket: string
|
||||
input_tokens?: number
|
||||
output_tokens?: number
|
||||
}
|
||||
|
||||
interface TimelineLatencyItem {
|
||||
bucket: string
|
||||
p50_ms?: number
|
||||
p95_ms?: number
|
||||
p99_ms?: number
|
||||
}
|
||||
|
||||
interface TimelineData {
|
||||
cost?: TimelineCostItem[]
|
||||
requests?: TimelineRequestItem[]
|
||||
tokens?: TimelineTokenItem[]
|
||||
latency_percentiles?: TimelineLatencyItem[]
|
||||
}
|
||||
|
||||
interface CostByModelItem {
|
||||
model?: string
|
||||
cost_total?: number
|
||||
share?: number
|
||||
}
|
||||
|
||||
interface CostByModelResponse {
|
||||
models?: CostByModelItem[]
|
||||
}
|
||||
|
||||
interface LatencyBucketItem {
|
||||
bucket: string
|
||||
count?: number
|
||||
}
|
||||
|
||||
interface LatencyDistributionResponse {
|
||||
buckets?: LatencyBucketItem[]
|
||||
}
|
||||
|
||||
interface CostByAgentItem {
|
||||
agent?: string
|
||||
cost_total?: number
|
||||
requests?: number
|
||||
}
|
||||
|
||||
interface CostByAgentResponse {
|
||||
agents?: CostByAgentItem[]
|
||||
}
|
||||
|
||||
interface AnalyticsData {
|
||||
analytics?: {
|
||||
timeline?: {
|
||||
resolution?: string
|
||||
hourly?: TimelineData
|
||||
daily?: TimelineData
|
||||
}
|
||||
cost_by_model?: CostByModelResponse
|
||||
latency_distribution?: LatencyDistributionResponse
|
||||
cost_by_agent?: CostByAgentResponse
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Types for transformed chart data
|
||||
// =============================================================================
|
||||
@@ -72,7 +148,7 @@ export function formatBucketLabel(bucket: string, resolution: 'day' | 'hour'): s
|
||||
/**
|
||||
* Transform analytics API response to chart-ready data
|
||||
*/
|
||||
export function transformAnalyticsData(data: any) {
|
||||
export function transformAnalyticsData(data: AnalyticsData | undefined) {
|
||||
if (!data?.analytics) {
|
||||
return {
|
||||
costTrends: [],
|
||||
@@ -85,7 +161,7 @@ export function transformAnalyticsData(data: any) {
|
||||
}
|
||||
|
||||
const analytics = data.analytics
|
||||
const resolution = analytics.timeline?.resolution || 'day'
|
||||
const resolution: 'day' | 'hour' = analytics.timeline?.resolution === 'hour' ? 'hour' : 'day'
|
||||
const timeline = resolution === 'hour' ? analytics.timeline?.hourly : analytics.timeline?.daily
|
||||
|
||||
return {
|
||||
@@ -102,7 +178,7 @@ export function transformAnalyticsData(data: any) {
|
||||
* Transform cost timeline to cost trend data
|
||||
*/
|
||||
function transformCostTrends(
|
||||
timeline: any,
|
||||
timeline: TimelineData | undefined,
|
||||
resolution: 'day' | 'hour'
|
||||
): CostTrendData[] {
|
||||
if (!timeline?.cost || !Array.isArray(timeline.cost)) {
|
||||
@@ -111,10 +187,10 @@ function transformCostTrends(
|
||||
|
||||
// Create requests lookup map
|
||||
const requestsMap = new Map<string, number>(
|
||||
(timeline.requests || []).map((r: any) => [r.bucket, r.requests])
|
||||
(timeline.requests || []).map((r: TimelineRequestItem) => [r.bucket, r.requests ?? 0])
|
||||
)
|
||||
|
||||
return timeline.cost.map((d: any) => ({
|
||||
return timeline.cost.map((d: TimelineCostItem) => ({
|
||||
date: formatBucketLabel(d.bucket, resolution),
|
||||
cost: d.cost_total || 0,
|
||||
requests: requestsMap.get(d.bucket) || 0,
|
||||
@@ -126,14 +202,14 @@ function transformCostTrends(
|
||||
* Transform token timeline to stacked bar chart data (flattened)
|
||||
*/
|
||||
function transformTokenUsage(
|
||||
timeline: any,
|
||||
timeline: TimelineData | undefined,
|
||||
resolution: 'day' | 'hour'
|
||||
): TokenUsageData[] {
|
||||
if (!timeline?.tokens || !Array.isArray(timeline.tokens)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return timeline.tokens.flatMap((d: any) => [
|
||||
return timeline.tokens.flatMap((d: TimelineTokenItem) => [
|
||||
{
|
||||
date: formatBucketLabel(d.bucket, resolution),
|
||||
type: 'Input' as const,
|
||||
@@ -150,12 +226,12 @@ function transformTokenUsage(
|
||||
/**
|
||||
* Transform cost by model to pie/donut chart data
|
||||
*/
|
||||
function transformCostByModel(costByModel: any): CostByModelData[] {
|
||||
function transformCostByModel(costByModel: CostByModelResponse | undefined): CostByModelData[] {
|
||||
if (!costByModel?.models || !Array.isArray(costByModel.models)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return costByModel.models.map((m: any, i: number) => ({
|
||||
return costByModel.models.map((m: CostByModelItem, i: number) => ({
|
||||
name: m.model?.split('/').pop() || m.model || 'Unknown',
|
||||
cost: m.cost_total || 0,
|
||||
value: Math.round((m.share || 0) * 100),
|
||||
@@ -169,7 +245,7 @@ function transformCostByModel(costByModel: any): CostByModelData[] {
|
||||
* UI: 0-2s, 2-5s, 5-10s, 10-20s, 20s+
|
||||
*/
|
||||
function transformLatencyDistribution(
|
||||
latencyDistribution: any
|
||||
latencyDistribution: LatencyDistributionResponse | undefined
|
||||
): LatencyDistributionData[] {
|
||||
if (!latencyDistribution?.buckets || !Array.isArray(latencyDistribution.buckets)) {
|
||||
return []
|
||||
@@ -183,7 +259,7 @@ function transformLatencyDistribution(
|
||||
'20s+': 0,
|
||||
}
|
||||
|
||||
latencyDistribution.buckets.forEach((b: any) => {
|
||||
latencyDistribution.buckets.forEach((b: LatencyBucketItem) => {
|
||||
switch (b.bucket) {
|
||||
case '0-1s':
|
||||
case '1-2s':
|
||||
@@ -217,14 +293,14 @@ function transformLatencyDistribution(
|
||||
* Transform latency percentiles to multi-line chart data (flattened)
|
||||
*/
|
||||
function transformLatencyPercentiles(
|
||||
timeline: any,
|
||||
timeline: TimelineData | undefined,
|
||||
resolution: 'day' | 'hour'
|
||||
): LatencyPercentilesData[] {
|
||||
if (!timeline?.latency_percentiles || !Array.isArray(timeline.latency_percentiles)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return timeline.latency_percentiles.flatMap((d: any) => [
|
||||
return timeline.latency_percentiles.flatMap((d: TimelineLatencyItem) => [
|
||||
{
|
||||
date: formatBucketLabel(d.bucket, resolution),
|
||||
percentile: 'P50' as const,
|
||||
@@ -246,15 +322,18 @@ function transformLatencyPercentiles(
|
||||
/**
|
||||
* Transform cost by agent to top agents list
|
||||
*/
|
||||
function transformTopAgents(costByAgent: any): TopAgentData[] {
|
||||
function transformTopAgents(costByAgent: CostByAgentResponse | undefined): TopAgentData[] {
|
||||
if (!costByAgent?.agents || !Array.isArray(costByAgent.agents)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return costByAgent.agents.map((a: any) => ({
|
||||
name: a.agent || 'Unknown',
|
||||
spend: a.cost_total || 0,
|
||||
requests: a.requests || 0,
|
||||
avgCost: a.requests > 0 ? (a.cost_total || 0) / a.requests : 0,
|
||||
}))
|
||||
return costByAgent.agents.map((a: CostByAgentItem) => {
|
||||
const requests = a.requests || 0
|
||||
return {
|
||||
name: a.agent || 'Unknown',
|
||||
spend: a.cost_total || 0,
|
||||
requests,
|
||||
avgCost: requests > 0 ? (a.cost_total || 0) / requests : 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,4 +33,5 @@ function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export { Badge, badgeVariants }
|
||||
|
||||
@@ -53,4 +53,5 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export { Button, buttonVariants }
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
getMetricsSummary,
|
||||
} from '@/services/agentControlApi'
|
||||
import { useSettingsStore } from '@/stores/settingsStore'
|
||||
import type { RawJsonData } from '@/types/agentControl'
|
||||
|
||||
// =============================================================================
|
||||
// Analytics Hook
|
||||
|
||||
@@ -10,7 +10,6 @@ import type {
|
||||
BudgetConfig,
|
||||
BudgetAlert,
|
||||
BudgetNotifications,
|
||||
RawJsonData,
|
||||
} from '@/types/agentControl'
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useQuery, useInfiniteQuery } from '@tanstack/react-query'
|
||||
import { getLogs, getLogsAggregated } from '@/services/agentControlApi'
|
||||
import type { RawJsonData } from '@/types/agentControl'
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
|
||||
@@ -93,9 +93,13 @@ export function useAgentStatus(
|
||||
let buffer = ''
|
||||
|
||||
// Read SSE stream
|
||||
while (true) {
|
||||
let streamActive = true
|
||||
while (streamActive) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
if (done) {
|
||||
streamActive = false
|
||||
continue
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useParams, useSearchParams } from 'react-router-dom'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { RegisterForm } from '@/components/auth/RegisterForm'
|
||||
import AdenLogo from '@/assets/aden-logo.svg'
|
||||
@@ -6,7 +6,6 @@ import { getOrgInfoByPath } from '@/services/authApi'
|
||||
|
||||
export function RegisterPage() {
|
||||
const { org } = useParams<{ org?: string }>()
|
||||
const [searchParams] = useSearchParams()
|
||||
const [orgName, setOrgName] = useState<string | undefined>()
|
||||
const [isLoadingOrg, setIsLoadingOrg] = useState(!!org)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user