mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[499f9eb1] Token Usage & Cost Analytics — Full-Stack Instrumentation, Persistence, and Visualization (#90)
* [cd2bf666] feat(usage): add token usage types, API client, hooks, and UI components (#87) (#88) - Append 5 TypeScript interfaces to src/types/index.ts: TokenUsageSnapshot, AgentUsageRow, UsageSession, UsageTimePoint, ModelUsageSlice - Create src/lib/api/usage.ts: Axios singleton + isMockMode guards for getUsageSnapshot, getUsageTimeSeries, getAgentUsage, getUsageSessions, getModelUsage - Create src/hooks/use-usage.ts: usageKeys factory + useUsageSnapshot, useUsageTimeSeries, useAgentUsage, useUsageSessions, useModelUsage hooks - Create UsageOverviewPanel (dashboard/usage-overview-panel.tsx): 6 metric rows with Skeleton loading state; week-over-week trend arrow for cost - Update CommandCenter: Metrics+Alerts row expanded from 2-col to 3-col grid adding UsageOverviewPanel - Create src/components/metrics/ folder: UsageTimeSeriesChart (recharts stacked AreaChart with var(--chart-1/2/3)), ModelUsageDonut (PieChart), AgentUsageChart and TeamUsageChart (BarChart), SessionsTable (sortable columns + 10-row Prev/Next pagination) - Update Metrics page: Token Usage & Costs section with 5 rows (summary cards, time series+donut, agent+team bar charts, projection+cache efficiency, sessions table) - Add usage mini-bar to AgentCard: token count + cost + progress bar; AgentGrid and Agents page pass agentUsageMap through - Install recharts 3.8.1 - Export all new symbols through their barrel index.ts files Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev> * [10372f0f] Implement full token usage instrumentation: DB migration, SDK endpoints, orchestrator hooks, analytics API, WebSocket events, dashboard integration (#86) (#89) * [10372f0f] feat(token-usage): add Alembic migration 026 for token usage tables Create agent_spawn_sessions, token_usage_snapshots, and daily_usage_rollups tables with correct BIGINT columns, indexes, and unique constraint. Chain: 025_agentrole_prompter → 026_token_usage_tables. * [10372f0f] feat(token-usage): add ORM table classes for token usage instrumentation Add AgentSpawnSessionTable, TokenUsageSnapshotTable, DailyUsageRollupTable to db/tables.py. Import BigInteger and Date from SQLAlchemy. All columns match the migration schema with BIGINT token counts and proper indexes. * [10372f0f] feat(billing): add pricing module with calculate_cost() function Create roboco/billing/__init__.py and roboco/billing/pricing.py with calculate_cost() supporting Claude opus/sonnet/haiku models with input/output/cache pricing. Unknown models return 0.0 without raising. * [10372f0f] feat(sdk): add POST /usage/report and GET /usage/status endpoints to agent SDK Extend _SessionState with token counters. Add TokenReportRequest and TokenUsageStatus models. POST /usage/report additively accumulates token counts; GET /usage/status returns current session totals for sweeper polling. * [10372f0f] feat(orchestrator): add token usage instrumentation hooks - _launch_spawn() calls _record_spawn_session() after successful container spawn - stop_agent() calls _finalize_spawn_session() before container removal - _run_sweep() calls _sweep_token_snapshots() and _sweep_daily_rollup() each tick - New methods: _record_spawn_session, _finalize_spawn_session, _sweep_token_snapshots, _sweep_daily_rollup in TOKEN USAGE section * [10372f0f] feat(api): add token usage analytics API with 7 endpoints Create roboco/services/usage.py (UsageService) and roboco/api/routes/usage.py. Endpoints: GET /api/usage/summary, /time-series, /by-agent, /by-team, /by-model, /projection, /cache-efficiency. Register in app.py. * [10372f0f] feat(dashboard): add usage_summary field to CEO dashboard Add UsageSummary schema (tokens_today, cost_today_usd) to dashboard schemas. Add usage_summary: UsageSummary | None to CEOOverview. Update get_ceo_overview() to populate usage_summary from daily_usage_rollups. * [10372f0f] fix(billing/tests): remove dead except block in _sweep_daily_rollup, add unit tests for pricing.py and services/usage.py - Remove unreachable `except Exception as e` block in orchestrator.py _sweep_daily_rollup() (lines 3376-3381) which referenced undefined `agent_id` and was copy-pasted from _sweep_token_snapshots by mistake - Add tests/unit/billing/test_pricing.py: 31 tests covering opus/sonnet/ haiku tiers with all 4 token types, unknown model → 0.0, empty string → 0.0, and substring-match priority (longer fragment wins) - Add tests/unit/services/test_usage.py: 25 tests covering get_summary trend_pct edge cases (prev=0, both=0, prev>0), get_by_agent/team/model pct_of_total summing to 100%, get_projection formula (avg_daily×30), and get_cache_efficiency hit-rate and cost_saved arithmetic - pricing.py: 100% coverage; services/usage.py: 83% coverage (>80% target) * [10372f0f] fix(usage): include cache tokens in time-series total_tokens to fix AC9 consistency violation get_time_series() previously computed total_tokens as tokens_input + tokens_output only. get_summary() includes all 4 token types (input + output + cache_read + cache_write). AC9 requires both endpoints to agree on their totals for the same period. Fix: add tokens_cache_read and tokens_cache_write to the SELECT query in get_time_series() and include them in the total_tokens calculation. Also adds 4 new unit tests in TestGetTimeSeries covering: - total_tokens includes cache_read and cache_write (the AC9 guard) - zero cache tokens still produces correct total - empty result returns empty list - required fields are present in each point * [10372f0f] fix(usage): remove unused imports and include cache tokens in breakdown totals (AC10) - Remove import math (F401 — never used) - Remove text from sqlalchemy import (F401 — never used) - Remove unused local calculate_cost import inside get_cache_efficiency (F401) - Add tokens_cache_read and tokens_cache_write to SELECT in get_by_agent, get_by_team, and get_by_model; update grand_total and per-item total to include all 4 token types so totals match get_summary() (AC10 fix) - Update test mock rows to include explicit tokens_cache_read=0 and tokens_cache_write=0 so they work with the fixed code - Add new test cases: test_cache_tokens_included_in_total_tokens and test_pct_of_total_sums_to_100_with_cache_tokens for each breakdown class --------- Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev> * [44b9eb1f] feat(usage): align frontend API client, TS types, and chart components to real backend contract (#92) (#94) Update all usage-related frontend code to match the actual FastAPI backend response shapes and endpoint paths: - panel/src/lib/api/usage.ts: rewrite all 7 API functions to use correct endpoint paths (/usage/summary, /usage/by-agent, /usage/by-model, /usage/by-team, /usage/time-series, /usage/projection, /usage/cache-efficiency); send period query param (24h/7d/30d not hours); mock generators produce data matching real backend shapes exactly; getUsageSessions returns [] in prod (no /usage/sessions endpoint exists) - panel/src/types/index.ts: replace TokenUsageSnapshot with UsageSummary (tokens_input/tokens_output/total_cost_usd/trend_pct); update AgentUsageRow to use agent_slug/total_tokens/cost_usd/pct_of_total; add TeamUsageRow, UsageProjection, CacheEfficiencyResponse; update UsageTimePoint to use bucket field; update UsageSession to use agent_slug - panel/src/hooks/use-usage.ts: rewrite all hooks to match new API and types; add useTeamUsage, useUsageProjection, useCacheEfficiency hooks - panel/src/components/metrics/usage-time-series-chart.tsx: use bucket field (not timestamp) for axis labels - panel/src/components/metrics/agent-usage-chart.tsx: use agent_slug and total_tokens (not agent_name/tokens_today) - panel/src/components/metrics/team-usage-chart.tsx: rewrite to accept TeamUsageRow[] from API directly - panel/src/components/metrics/model-usage-donut.tsx: use total_tokens, cost_usd, pct_of_total (not tokens/cost/percentage) - panel/src/components/metrics/sessions-table.tsx: use agent_slug, sort keys updated - panel/src/components/dashboard/usage-overview-panel.tsx: use useUsageSummary with tokens_input/tokens_output/total_cost_usd/trend_pct - panel/src/app/(dashboard)/metrics/page.tsx: wire all new hooks, add TeamUsageChart, ProjectionCard, CacheEfficiencyCard with correct types - panel/src/app/(dashboard)/agents/page.tsx: key agentUsageMap by agent_slug - panel/src/components/agents/agent-card.tsx: use total_tokens and cost_usd Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev> * [2161b832] fix: SDK_PORT constant, stop_agent lock refactor, usage_session_id binding, rollup 7-day window (#93) (#95) - Add SDK_PORT = 9000 module-level constant to orchestrator.py; replace hardcoded 9000 in _sweep_budget_exceeded URL with SDK_PORT - Add UUID to TYPE_CHECKING imports to satisfy ruff F821 - Refactor stop_agent: call _finalize_spawn_session BEFORE acquiring self._lock so the SDK HTTP round-trip does not hold the lock - Add usage_session_id: UUID | None field to AgentInstance dataclass - Change _record_spawn_session to return UUID | None; wire return value back to instance.usage_session_id in _launch_spawn - Update _finalize_spawn_session to use WHERE id=usage_session_id for direct session row lookup when usage_session_id is not None - Add started_at >= (now_utc - 7 days) filter to _sweep_daily_rollup aggregate query to avoid re-aggregating all-time history each sweep Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev> * [2e0759e1] fix: pricing accuracy, import ordering, session-id binding, rollup cleanup, write-hook tests (#97) (#98) - pricing.py: correct claude-opus-4 prices (5/25/0.50/6.25 not 15/75/1.5/3.75) and haiku family prices (1/5/0.10/1.25 not 0.8/4/0.08/0.20); add Ollama zero-cost early-return; add structlog warning for unmatched model names - app.py: move usage_router import before routes.v1 block (ruff isort fix) - orchestrator.py _sweep_daily_rollup: remove unused calculate_cost import; add blank line between stdlib (uuid4) and third-party (sqlalchemy) imports - orchestrator.py _sweep_token_snapshots: prefer direct lookup by instance.usage_session_id; fall back to agent_slug heuristic only when None - tests: add test_sweep_daily_rollup_inserts_new_row and test_stop_agent_finalizes_before_lock to test_orchestrator_write_hooks.py - usage.py, routes/usage.py, stream_bus.py, test files: ruff format/lint fixes Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev> * Mypy compliance * fix(migrations,tests): linearize forked migration chain + correct ceo_reject coordination-root expectation The master merge brought in 026_completed_dependency_ids alongside the rework's 026_token_usage_tables — both off 025, forking the alembic head and breaking the enum-parity test. Rebase token-usage onto 026_completed_dependency_ids (linear chain, single head). Also: test_ceo_reject_routes_coordination_task_to_main_pm asserted the old NEEDS_REVISION behavior; the lifecycle fix correctly routes a coordination root to PENDING (Main PM's claim source). Update the assertion. --------- Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev> Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev> Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
co-authored by
Frontend Developer 1
Backend Developer 1
Renn F
parent
93c6ef8a57
commit
b3057628b0
@@ -42,6 +42,7 @@
|
||||
"react-dom": "19.2.3",
|
||||
"react-hook-form": "^7.71.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"recharts": "^3.8.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
|
||||
Generated
+338
-7
@@ -107,6 +107,9 @@ importers:
|
||||
react-markdown:
|
||||
specifier: ^10.1.0
|
||||
version: 10.1.0(@types/react@19.2.8)(react@19.2.3)
|
||||
recharts:
|
||||
specifier: ^3.8.1
|
||||
version: 3.8.1(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1)
|
||||
remark-gfm:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
@@ -121,7 +124,7 @@ importers:
|
||||
version: 4.3.5
|
||||
zustand:
|
||||
specifier: ^5.0.10
|
||||
version: 5.0.10(@types/react@19.2.8)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))
|
||||
version: 5.0.10(@types/react@19.2.8)(immer@11.1.8)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))
|
||||
devDependencies:
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4
|
||||
@@ -362,89 +365,105 @@ packages:
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||
@@ -511,24 +530,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.1.1':
|
||||
resolution: {integrity: sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.1.1':
|
||||
resolution: {integrity: sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-x64-musl@16.1.1':
|
||||
resolution: {integrity: sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.1.1':
|
||||
resolution: {integrity: sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==}
|
||||
@@ -1071,9 +1094,23 @@ packages:
|
||||
'@radix-ui/rect@1.1.1':
|
||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
||||
|
||||
'@reduxjs/toolkit@2.12.0':
|
||||
resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==}
|
||||
peerDependencies:
|
||||
react: ^16.9.0 || ^17.0.0 || ^18 || ^19
|
||||
react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
react-redux:
|
||||
optional: true
|
||||
|
||||
'@rtsao/scc@1.1.0':
|
||||
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
||||
|
||||
'@standard-schema/spec@1.1.0':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
|
||||
'@standard-schema/utils@0.3.0':
|
||||
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
||||
|
||||
@@ -1118,24 +1155,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
|
||||
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
|
||||
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
|
||||
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
|
||||
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
|
||||
@@ -1193,6 +1234,33 @@ packages:
|
||||
'@tybys/wasm-util@0.10.1':
|
||||
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
||||
|
||||
'@types/d3-array@3.2.2':
|
||||
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
|
||||
|
||||
'@types/d3-color@3.1.3':
|
||||
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
|
||||
|
||||
'@types/d3-ease@3.0.2':
|
||||
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
|
||||
|
||||
'@types/d3-interpolate@3.0.4':
|
||||
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
|
||||
|
||||
'@types/d3-path@3.1.1':
|
||||
resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
|
||||
|
||||
'@types/d3-scale@4.0.9':
|
||||
resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
|
||||
|
||||
'@types/d3-shape@3.1.8':
|
||||
resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==}
|
||||
|
||||
'@types/d3-time@3.0.4':
|
||||
resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
|
||||
|
||||
'@types/d3-timer@3.0.2':
|
||||
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||
|
||||
@@ -1234,6 +1302,9 @@ packages:
|
||||
'@types/unist@3.0.3':
|
||||
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
||||
|
||||
'@types/use-sync-external-store@0.0.6':
|
||||
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.49.0':
|
||||
resolution: {integrity: sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -1295,6 +1366,7 @@ packages:
|
||||
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
|
||||
|
||||
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
|
||||
resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
|
||||
@@ -1335,41 +1407,49 @@ packages:
|
||||
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
|
||||
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
||||
@@ -1581,6 +1661,50 @@ packages:
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
d3-array@3.2.4:
|
||||
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-color@3.1.0:
|
||||
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-ease@3.0.1:
|
||||
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-format@3.1.2:
|
||||
resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-interpolate@3.0.1:
|
||||
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-path@3.1.0:
|
||||
resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-scale@4.0.2:
|
||||
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-shape@3.2.0:
|
||||
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-time-format@4.1.0:
|
||||
resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-time@3.1.0:
|
||||
resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-timer@3.0.1:
|
||||
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
damerau-levenshtein@1.0.8:
|
||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||
|
||||
@@ -1616,6 +1740,9 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
decimal.js-light@2.5.1:
|
||||
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
|
||||
|
||||
decode-named-character-reference@1.2.0:
|
||||
resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
|
||||
|
||||
@@ -1698,6 +1825,9 @@ packages:
|
||||
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-toolkit@1.47.0:
|
||||
resolution: {integrity: sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==}
|
||||
|
||||
escalade@3.2.0:
|
||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1829,6 +1959,9 @@ packages:
|
||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
eventemitter3@5.0.4:
|
||||
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
||||
|
||||
extend@3.0.2:
|
||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||
|
||||
@@ -2007,6 +2140,12 @@ packages:
|
||||
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
immer@10.2.0:
|
||||
resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==}
|
||||
|
||||
immer@11.1.8:
|
||||
resolution: {integrity: sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==}
|
||||
|
||||
import-fresh@3.3.1:
|
||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2022,6 +2161,10 @@ packages:
|
||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
internmap@2.0.3:
|
||||
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
is-alphabetical@2.0.1:
|
||||
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
|
||||
|
||||
@@ -2238,24 +2381,28 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.30.2:
|
||||
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.2:
|
||||
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.2:
|
||||
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.30.2:
|
||||
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
|
||||
@@ -2639,6 +2786,18 @@ packages:
|
||||
'@types/react': '>=18'
|
||||
react: '>=18'
|
||||
|
||||
react-redux@9.3.0:
|
||||
resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==}
|
||||
peerDependencies:
|
||||
'@types/react': ^18.2.25 || ^19
|
||||
react: ^18.0 || ^19
|
||||
redux: ^5.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
redux:
|
||||
optional: true
|
||||
|
||||
react-remove-scroll-bar@2.3.8:
|
||||
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -2673,6 +2832,22 @@ packages:
|
||||
resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
recharts@3.8.1:
|
||||
resolution: {integrity: sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
redux-thunk@3.1.0:
|
||||
resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
|
||||
peerDependencies:
|
||||
redux: ^5.0.0
|
||||
|
||||
redux@5.0.1:
|
||||
resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
|
||||
|
||||
reflect.getprototypeof@1.0.10:
|
||||
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2693,6 +2868,9 @@ packages:
|
||||
remark-stringify@11.0.0:
|
||||
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
|
||||
|
||||
reselect@5.1.1:
|
||||
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
|
||||
|
||||
resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -2871,6 +3049,9 @@ packages:
|
||||
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tiny-invariant@1.3.3:
|
||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -3003,6 +3184,9 @@ packages:
|
||||
vfile@6.0.3:
|
||||
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
|
||||
|
||||
victory-vendor@37.3.6:
|
||||
resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
|
||||
|
||||
which-boxed-primitive@1.1.1:
|
||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3971,8 +4155,22 @@ snapshots:
|
||||
|
||||
'@radix-ui/rect@1.1.1': {}
|
||||
|
||||
'@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.1.0
|
||||
'@standard-schema/utils': 0.3.0
|
||||
immer: 11.1.8
|
||||
redux: 5.0.1
|
||||
redux-thunk: 3.1.0(redux@5.0.1)
|
||||
reselect: 5.1.1
|
||||
optionalDependencies:
|
||||
react: 19.2.3
|
||||
react-redux: 9.3.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1)
|
||||
|
||||
'@rtsao/scc@1.1.0': {}
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@standard-schema/utils@0.3.0': {}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
@@ -4073,6 +4271,30 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@types/d3-array@3.2.2': {}
|
||||
|
||||
'@types/d3-color@3.1.3': {}
|
||||
|
||||
'@types/d3-ease@3.0.2': {}
|
||||
|
||||
'@types/d3-interpolate@3.0.4':
|
||||
dependencies:
|
||||
'@types/d3-color': 3.1.3
|
||||
|
||||
'@types/d3-path@3.1.1': {}
|
||||
|
||||
'@types/d3-scale@4.0.9':
|
||||
dependencies:
|
||||
'@types/d3-time': 3.0.4
|
||||
|
||||
'@types/d3-shape@3.1.8':
|
||||
dependencies:
|
||||
'@types/d3-path': 3.1.1
|
||||
|
||||
'@types/d3-time@3.0.4': {}
|
||||
|
||||
'@types/d3-timer@3.0.2': {}
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
@@ -4113,6 +4335,8 @@ snapshots:
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
|
||||
'@types/use-sync-external-store@0.0.6': {}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
@@ -4476,6 +4700,44 @@ snapshots:
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
d3-array@3.2.4:
|
||||
dependencies:
|
||||
internmap: 2.0.3
|
||||
|
||||
d3-color@3.1.0: {}
|
||||
|
||||
d3-ease@3.0.1: {}
|
||||
|
||||
d3-format@3.1.2: {}
|
||||
|
||||
d3-interpolate@3.0.1:
|
||||
dependencies:
|
||||
d3-color: 3.1.0
|
||||
|
||||
d3-path@3.1.0: {}
|
||||
|
||||
d3-scale@4.0.2:
|
||||
dependencies:
|
||||
d3-array: 3.2.4
|
||||
d3-format: 3.1.2
|
||||
d3-interpolate: 3.0.1
|
||||
d3-time: 3.1.0
|
||||
d3-time-format: 4.1.0
|
||||
|
||||
d3-shape@3.2.0:
|
||||
dependencies:
|
||||
d3-path: 3.1.0
|
||||
|
||||
d3-time-format@4.1.0:
|
||||
dependencies:
|
||||
d3-time: 3.1.0
|
||||
|
||||
d3-time@3.1.0:
|
||||
dependencies:
|
||||
d3-array: 3.2.4
|
||||
|
||||
d3-timer@3.0.1: {}
|
||||
|
||||
damerau-levenshtein@1.0.8: {}
|
||||
|
||||
data-view-buffer@1.0.2:
|
||||
@@ -4506,6 +4768,8 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
decimal.js-light@2.5.1: {}
|
||||
|
||||
decode-named-character-reference@1.2.0:
|
||||
dependencies:
|
||||
character-entities: 2.0.2
|
||||
@@ -4656,6 +4920,8 @@ snapshots:
|
||||
is-date-object: 1.1.0
|
||||
is-symbol: 1.1.1
|
||||
|
||||
es-toolkit@1.47.0: {}
|
||||
|
||||
escalade@3.2.0: {}
|
||||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
@@ -4667,7 +4933,7 @@ snapshots:
|
||||
'@next/eslint-plugin-next': 16.1.1
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
|
||||
@@ -4690,7 +4956,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)):
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3
|
||||
@@ -4705,14 +4971,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -4727,7 +4993,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
@@ -4867,6 +5133,8 @@ snapshots:
|
||||
|
||||
esutils@2.0.3: {}
|
||||
|
||||
eventemitter3@5.0.4: {}
|
||||
|
||||
extend@3.0.2: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
@@ -5051,6 +5319,10 @@ snapshots:
|
||||
|
||||
ignore@7.0.5: {}
|
||||
|
||||
immer@10.2.0: {}
|
||||
|
||||
immer@11.1.8: {}
|
||||
|
||||
import-fresh@3.3.1:
|
||||
dependencies:
|
||||
parent-module: 1.0.1
|
||||
@@ -5066,6 +5338,8 @@ snapshots:
|
||||
hasown: 2.0.2
|
||||
side-channel: 1.1.0
|
||||
|
||||
internmap@2.0.3: {}
|
||||
|
||||
is-alphabetical@2.0.1: {}
|
||||
|
||||
is-alphanumerical@2.0.1:
|
||||
@@ -5886,6 +6160,15 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
react-redux@9.3.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1):
|
||||
dependencies:
|
||||
'@types/use-sync-external-store': 0.0.6
|
||||
react: 19.2.3
|
||||
use-sync-external-store: 1.6.0(react@19.2.3)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.8
|
||||
redux: 5.0.1
|
||||
|
||||
react-remove-scroll-bar@2.3.8(@types/react@19.2.8)(react@19.2.3):
|
||||
dependencies:
|
||||
react: 19.2.3
|
||||
@@ -5915,6 +6198,32 @@ snapshots:
|
||||
|
||||
react@19.2.3: {}
|
||||
|
||||
recharts@3.8.1(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1):
|
||||
dependencies:
|
||||
'@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1))(react@19.2.3)
|
||||
clsx: 2.1.1
|
||||
decimal.js-light: 2.5.1
|
||||
es-toolkit: 1.47.0
|
||||
eventemitter3: 5.0.4
|
||||
immer: 10.2.0
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
react-is: 16.13.1
|
||||
react-redux: 9.3.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1)
|
||||
reselect: 5.1.1
|
||||
tiny-invariant: 1.3.3
|
||||
use-sync-external-store: 1.6.0(react@19.2.3)
|
||||
victory-vendor: 37.3.6
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
- redux
|
||||
|
||||
redux-thunk@3.1.0(redux@5.0.1):
|
||||
dependencies:
|
||||
redux: 5.0.1
|
||||
|
||||
redux@5.0.1: {}
|
||||
|
||||
reflect.getprototypeof@1.0.10:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
@@ -5969,6 +6278,8 @@ snapshots:
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
unified: 11.0.5
|
||||
|
||||
reselect@5.1.1: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
@@ -6206,6 +6517,8 @@ snapshots:
|
||||
|
||||
tapable@2.3.0: {}
|
||||
|
||||
tiny-invariant@1.3.3: {}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
@@ -6391,6 +6704,23 @@ snapshots:
|
||||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
victory-vendor@37.3.6:
|
||||
dependencies:
|
||||
'@types/d3-array': 3.2.2
|
||||
'@types/d3-ease': 3.0.2
|
||||
'@types/d3-interpolate': 3.0.4
|
||||
'@types/d3-scale': 4.0.9
|
||||
'@types/d3-shape': 3.1.8
|
||||
'@types/d3-time': 3.0.4
|
||||
'@types/d3-timer': 3.0.2
|
||||
d3-array: 3.2.4
|
||||
d3-ease: 3.0.1
|
||||
d3-interpolate: 3.0.1
|
||||
d3-scale: 4.0.2
|
||||
d3-shape: 3.2.0
|
||||
d3-time: 3.1.0
|
||||
d3-timer: 3.0.1
|
||||
|
||||
which-boxed-primitive@1.1.1:
|
||||
dependencies:
|
||||
is-bigint: 1.1.0
|
||||
@@ -6448,9 +6778,10 @@ snapshots:
|
||||
|
||||
zod@4.3.5: {}
|
||||
|
||||
zustand@5.0.10(@types/react@19.2.8)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)):
|
||||
zustand@5.0.10(@types/react@19.2.8)(immer@11.1.8)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)):
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.8
|
||||
immer: 11.1.8
|
||||
react: 19.2.3
|
||||
use-sync-external-store: 1.6.0(react@19.2.3)
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
useWaitingAgents,
|
||||
useAgentDefinitions,
|
||||
} from "@/hooks/use-agents";
|
||||
import { AgentStatusResponse } from "@/types";
|
||||
import { useAgentUsage } from "@/hooks/use-usage";
|
||||
import { AgentStatusResponse, AgentUsageRow } from "@/types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { OfflineState } from "@/components/ui/offline-state";
|
||||
@@ -27,6 +28,7 @@ export default function AgentsPage() {
|
||||
const { data: agents = [], isLoading: agentsLoading } = useAgentDefinitions();
|
||||
const { data: status, isLoading, error, refetch } = useOrchestratorStatus();
|
||||
const { data: waitingAgents } = useWaitingAgents();
|
||||
const { data: usageRows } = useAgentUsage();
|
||||
|
||||
// Check if it's a connection error (backend not running)
|
||||
const isOffline = error && (
|
||||
@@ -46,6 +48,15 @@ export default function AgentsPage() {
|
||||
return result;
|
||||
}, [status]);
|
||||
|
||||
// Convert usage rows to a record keyed by agent_slug
|
||||
const agentUsageMap = useMemo(() => {
|
||||
const result: Record<string, AgentUsageRow> = {};
|
||||
for (const row of usageRows ?? []) {
|
||||
result[row.agent_slug] = row;
|
||||
}
|
||||
return result;
|
||||
}, [usageRows]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
@@ -83,6 +94,7 @@ export default function AgentsPage() {
|
||||
title="Board"
|
||||
agents={getBoardAgents(agents)}
|
||||
agentStatuses={agentStatuses}
|
||||
agentUsage={agentUsageMap}
|
||||
isLoading={(isLoading || agentsLoading) && !isOffline}
|
||||
columns={4}
|
||||
/>
|
||||
@@ -91,6 +103,7 @@ export default function AgentsPage() {
|
||||
title="Main PM"
|
||||
agents={getMainPm(agents)}
|
||||
agentStatuses={agentStatuses}
|
||||
agentUsage={agentUsageMap}
|
||||
isLoading={(isLoading || agentsLoading) && !isOffline}
|
||||
columns={4}
|
||||
/>
|
||||
@@ -99,6 +112,7 @@ export default function AgentsPage() {
|
||||
title="Backend Cell"
|
||||
agents={getBackendAgents(agents)}
|
||||
agentStatuses={agentStatuses}
|
||||
agentUsage={agentUsageMap}
|
||||
isLoading={(isLoading || agentsLoading) && !isOffline}
|
||||
columns={5}
|
||||
/>
|
||||
@@ -107,6 +121,7 @@ export default function AgentsPage() {
|
||||
title="Frontend Cell"
|
||||
agents={getFrontendAgents(agents)}
|
||||
agentStatuses={agentStatuses}
|
||||
agentUsage={agentUsageMap}
|
||||
isLoading={(isLoading || agentsLoading) && !isOffline}
|
||||
columns={5}
|
||||
/>
|
||||
@@ -115,6 +130,7 @@ export default function AgentsPage() {
|
||||
title="UX/UI Cell"
|
||||
agents={getUxAgents(agents)}
|
||||
agentStatuses={agentStatuses}
|
||||
agentUsage={agentUsageMap}
|
||||
isLoading={(isLoading || agentsLoading) && !isOffline}
|
||||
columns={4}
|
||||
/>
|
||||
|
||||
@@ -2,14 +2,33 @@
|
||||
|
||||
import { useOrchestratorStatus } from "@/hooks/use-agents";
|
||||
import { useTasks } from "@/hooks/use-tasks";
|
||||
import {
|
||||
useUsageSummary,
|
||||
useUsageTimeSeries,
|
||||
useAgentUsage,
|
||||
useTeamUsage,
|
||||
useModelUsage,
|
||||
useUsageProjection,
|
||||
useCacheEfficiency,
|
||||
useUsageSessions,
|
||||
} from "@/hooks/use-usage";
|
||||
import { TaskStatus, Team } from "@/types";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { OfflineState } from "@/components/ui/offline-state";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
UsageTimeSeriesChart,
|
||||
ModelUsageDonut,
|
||||
AgentUsageChart,
|
||||
TeamUsageChart,
|
||||
SessionsTable,
|
||||
} from "@/components/metrics";
|
||||
import {
|
||||
Activity,
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
Clock,
|
||||
AlertTriangle,
|
||||
Users,
|
||||
@@ -18,6 +37,8 @@ import {
|
||||
RefreshCw,
|
||||
Zap,
|
||||
Timer,
|
||||
Coins,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
|
||||
interface MetricCardProps {
|
||||
@@ -295,8 +316,218 @@ export default function MetricsPage() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── Token Usage & Costs ─────────────────────────────────── */}
|
||||
<TokenUsageCostsSection />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TOKEN USAGE & COSTS SECTION
|
||||
// =============================================================================
|
||||
|
||||
function TokenUsageCostsSection() {
|
||||
const { data: summary, isLoading: loadingSnap } = useUsageSummary("24h");
|
||||
const { data: timeSeries, isLoading: loadingTS } = useUsageTimeSeries("24h");
|
||||
const { data: agentUsage, isLoading: loadingAgents } = useAgentUsage("24h");
|
||||
const { data: teamUsage, isLoading: loadingTeams } = useTeamUsage("24h");
|
||||
const { data: sessions, isLoading: loadingSessions } = useUsageSessions(100);
|
||||
const { data: modelUsage, isLoading: loadingModels } = useModelUsage("24h");
|
||||
const { data: projection, isLoading: loadingProj } = useUsageProjection();
|
||||
const { data: cacheStats, isLoading: loadingCache } = useCacheEfficiency("24h");
|
||||
|
||||
const trendUp = (summary?.trend_pct ?? 0) >= 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-semibold">Token Usage & Costs</h2>
|
||||
|
||||
{/* Row 1 — Summary cards */}
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
||||
<SummaryCard
|
||||
title="Tokens Input"
|
||||
value={summary ? fmtTokens(summary.tokens_input) : undefined}
|
||||
icon={<Zap className="h-4 w-4 text-yellow-500" />}
|
||||
isLoading={loadingSnap}
|
||||
/>
|
||||
<SummaryCard
|
||||
title="Tokens Output"
|
||||
value={summary ? fmtTokens(summary.tokens_output) : undefined}
|
||||
icon={<Zap className="h-4 w-4 text-blue-500" />}
|
||||
isLoading={loadingSnap}
|
||||
/>
|
||||
<SummaryCard
|
||||
title="Total Cost (24h)"
|
||||
value={summary ? "$" + summary.total_cost_usd.toFixed(4) : undefined}
|
||||
icon={<Coins className="h-4 w-4 text-green-500" />}
|
||||
isLoading={loadingSnap}
|
||||
/>
|
||||
<SummaryCard
|
||||
title="Trend vs Prior"
|
||||
value={summary ? (trendUp ? "+" : "") + summary.trend_pct.toFixed(1) + "%" : undefined}
|
||||
icon={
|
||||
trendUp ? (
|
||||
<TrendingUp className="h-4 w-4 text-red-500" />
|
||||
) : (
|
||||
<TrendingDown className="h-4 w-4 text-green-500" />
|
||||
)
|
||||
}
|
||||
isLoading={loadingSnap}
|
||||
/>
|
||||
<SummaryCard
|
||||
title="Total Tokens"
|
||||
value={summary ? fmtTokens(summary.total_tokens) : undefined}
|
||||
icon={<Activity className="h-4 w-4 text-blue-500" />}
|
||||
isLoading={loadingSnap}
|
||||
/>
|
||||
<SummaryCard
|
||||
title="Cache Saved"
|
||||
value={cacheStats ? "$" + cacheStats.cost_saved_by_cache_usd.toFixed(4) : undefined}
|
||||
icon={<Sparkles className="h-4 w-4 text-purple-500" />}
|
||||
isLoading={loadingCache}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Row 2 — Time series + model donut */}
|
||||
<div className="grid gap-4 lg:grid-cols-3">
|
||||
<div className="lg:col-span-2">
|
||||
<UsageTimeSeriesChart data={timeSeries} isLoading={loadingTS} />
|
||||
</div>
|
||||
<ModelUsageDonut data={modelUsage} isLoading={loadingModels} />
|
||||
</div>
|
||||
|
||||
{/* Row 3 — Agent bar + team bar */}
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<AgentUsageChart data={agentUsage} isLoading={loadingAgents} />
|
||||
<TeamUsageChart data={teamUsage} isLoading={loadingTeams} />
|
||||
</div>
|
||||
|
||||
{/* Row 4 — Projection + cache efficiency */}
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<ProjectionCard projection={projection} isLoading={loadingProj} />
|
||||
<CacheEfficiencyCard cacheStats={cacheStats} isLoading={loadingCache} />
|
||||
</div>
|
||||
|
||||
{/* Row 5 — Sessions table (mock-mode only; empty in production) */}
|
||||
<SessionsTable data={sessions} isLoading={loadingSessions} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Helper sub-components ────────────────────────────────────────────────────
|
||||
|
||||
function fmtTokens(n: number): string {
|
||||
if (n >= 1_000_000) return (n / 1_000_000).toFixed(2) + "M";
|
||||
if (n >= 1_000) return (n / 1_000).toFixed(1) + "K";
|
||||
return String(n);
|
||||
}
|
||||
|
||||
interface SummaryCardProps {
|
||||
title: string;
|
||||
value: string | undefined;
|
||||
icon: React.ReactNode;
|
||||
trend?: { dir: "up" | "down"; label: string };
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
function SummaryCard({ title, value, icon, trend, isLoading }: SummaryCardProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">{title}</CardTitle>
|
||||
{icon}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-7 w-24" />
|
||||
) : (
|
||||
<>
|
||||
<div className="text-2xl font-bold">{value ?? "—"}</div>
|
||||
{trend && (
|
||||
<p
|
||||
className={
|
||||
"text-xs mt-1 " +
|
||||
(trend.dir === "up" ? "text-red-500" : "text-green-500")
|
||||
}
|
||||
>
|
||||
{trend.label}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
import type { UsageProjection as UP, CacheEfficiencyResponse as CER } from "@/types";
|
||||
|
||||
interface ProjectionCardProps {
|
||||
projection: UP | undefined;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
function ProjectionCard({ projection, isLoading }: ProjectionCardProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<TrendingUp className="h-4 w-4 text-blue-500" />
|
||||
Monthly Projection
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-10 w-full" />
|
||||
) : (
|
||||
<div>
|
||||
<div className="text-3xl font-bold">
|
||||
{projection != null ? "$" + projection.projected_monthly_cost_usd.toFixed(2) : "—"}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Based on {projection?.basis_days ?? 7}-day rolling average ($
|
||||
{projection?.avg_daily_cost_usd.toFixed(4) ?? "—"}/day)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
interface CacheEfficiencyCardProps {
|
||||
cacheStats: CER | undefined;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
function CacheEfficiencyCard({ cacheStats, isLoading }: CacheEfficiencyCardProps) {
|
||||
const pct = cacheStats ? cacheStats.cache_hit_rate * 100 : 0;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Sparkles className="h-4 w-4 text-purple-500" />
|
||||
Cache Efficiency
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-10 w-full" />
|
||||
) : (
|
||||
<div>
|
||||
<div className="text-3xl font-bold">{pct.toFixed(1)}%</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{cacheStats ? fmtTokens(cacheStats.tokens_cache_read) : "—"} cache reads ·
|
||||
saved ${cacheStats?.cost_saved_by_cache_usd.toFixed(4) ?? "—"}
|
||||
</p>
|
||||
<Progress value={pct} className="mt-2" />
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,13 +17,15 @@ import { MoreHorizontal, Activity, Square } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { AgentStateBadge } from "./agent-state-badge";
|
||||
import { SpawnAgentDialog } from "./spawn-agent-dialog";
|
||||
import type { AgentUsageRow } from "@/types";
|
||||
|
||||
interface AgentCardProps {
|
||||
agent: AgentDefinition;
|
||||
agentStatus: AgentStatusResponse | null;
|
||||
usageRow?: AgentUsageRow | null;
|
||||
}
|
||||
|
||||
export function AgentCard({ agent, agentStatus }: AgentCardProps) {
|
||||
export function AgentCard({ agent, agentStatus, usageRow }: AgentCardProps) {
|
||||
const stopAgent = useStopAgent();
|
||||
const state = agentStatus?.state || "stopped";
|
||||
const isActive = ["running", "ready", "starting", "waiting_long"].includes(state);
|
||||
@@ -100,6 +102,30 @@ export function AgentCard({ agent, agentStatus }: AgentCardProps) {
|
||||
Errors: {agentStatus.error_count}
|
||||
</p>
|
||||
)}
|
||||
{usageRow && (
|
||||
<div className="mt-3 pt-2 border-t">
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground mb-1">
|
||||
<span>
|
||||
{usageRow.total_tokens >= 1_000
|
||||
? (usageRow.total_tokens / 1_000).toFixed(1) + "K"
|
||||
: String(usageRow.total_tokens)}{" "}
|
||||
tokens
|
||||
</span>
|
||||
<span className="font-medium text-foreground">
|
||||
${usageRow.cost_usd.toFixed(4)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-1.5 overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-[var(--chart-1)]"
|
||||
style={{
|
||||
width:
|
||||
Math.min(100, (usageRow.total_tokens / 30_000) * 100) + "%",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AgentStatusResponse } from "@/types";
|
||||
import { AgentStatusResponse, AgentUsageRow } from "@/types";
|
||||
import { AgentDefinition } from "@/lib/agent-definitions";
|
||||
import { Card, CardHeader } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -8,6 +8,7 @@ interface AgentGridProps {
|
||||
title: string;
|
||||
agents: AgentDefinition[];
|
||||
agentStatuses: Record<string, AgentStatusResponse>;
|
||||
agentUsage?: Record<string, AgentUsageRow>;
|
||||
isLoading: boolean;
|
||||
columns?: number;
|
||||
}
|
||||
@@ -16,8 +17,9 @@ export function AgentGrid({
|
||||
title,
|
||||
agents,
|
||||
agentStatuses,
|
||||
agentUsage,
|
||||
isLoading,
|
||||
columns = 4
|
||||
columns = 4,
|
||||
}: AgentGridProps) {
|
||||
const gridCols = {
|
||||
3: "md:grid-cols-3",
|
||||
@@ -44,6 +46,7 @@ export function AgentGrid({
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
agentStatus={agentStatuses[agent.id] || null}
|
||||
usageRow={agentUsage?.[agent.id] ?? null}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { QuickActionsBar } from "./quick-actions-bar";
|
||||
import { CeoApprovalQueue } from "./ceo-approval-queue";
|
||||
import type { Activity } from "./activity-item";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { UsageOverviewPanel } from "./usage-overview-panel";
|
||||
import { RefreshCw, Settings } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
@@ -61,13 +62,14 @@ export function CommandCenter() {
|
||||
<CeoApprovalQueue />
|
||||
</section>
|
||||
|
||||
{/* Metrics and Alerts Row */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Metrics, Alerts, and Usage Row */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<KeyMetricsPanel
|
||||
metrics={overview?.key_metrics}
|
||||
isLoading={loadingOverview}
|
||||
/>
|
||||
<AuditorAlertsPanel alerts={flags} isLoading={loadingFlags} />
|
||||
<UsageOverviewPanel />
|
||||
</div>
|
||||
|
||||
{/* Blockers and Activity Row */}
|
||||
|
||||
@@ -9,3 +9,4 @@ export { ActivityItem } from "./activity-item";
|
||||
export { QuickActionsBar } from "./quick-actions-bar";
|
||||
export { HealthIndicator } from "./health-indicator";
|
||||
export { CeoApprovalQueue } from "./ceo-approval-queue";
|
||||
export { UsageOverviewPanel } from "./usage-overview-panel";
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useUsageSummary } from "@/hooks/use-usage";
|
||||
import { Coins, TrendingUp, TrendingDown, Zap, Activity } from "lucide-react";
|
||||
|
||||
function fmt(n: number, decimals = 0): string {
|
||||
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
|
||||
if (n >= 1_000) return (n / 1_000).toFixed(1) + "K";
|
||||
return n.toFixed(decimals);
|
||||
}
|
||||
|
||||
function fmtCost(n: number): string {
|
||||
return "$" + n.toFixed(2);
|
||||
}
|
||||
|
||||
interface MetricRowProps {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
value: string;
|
||||
sub?: React.ReactNode;
|
||||
}
|
||||
|
||||
function MetricRow({ icon, label, value, sub }: MetricRowProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
{icon}
|
||||
{label}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-semibold text-sm">{value}</span>
|
||||
{sub}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function UsageOverviewPanel() {
|
||||
const { data: summary, isLoading } = useUsageSummary("24h");
|
||||
|
||||
const trendUp = (summary?.trend_pct ?? 0) >= 0;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Coins className="h-5 w-5" />
|
||||
Token Usage & Cost
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-6" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
<MetricRow
|
||||
icon={<Zap className="h-4 w-4" />}
|
||||
label="Tokens (input)"
|
||||
value={summary ? fmt(summary.tokens_input) : "—"}
|
||||
/>
|
||||
<MetricRow
|
||||
icon={<Zap className="h-4 w-4 text-muted-foreground" />}
|
||||
label="Tokens (output)"
|
||||
value={summary ? fmt(summary.tokens_output) : "—"}
|
||||
/>
|
||||
<MetricRow
|
||||
icon={<Coins className="h-4 w-4" />}
|
||||
label="Total cost"
|
||||
value={summary ? fmtCost(summary.total_cost_usd) : "—"}
|
||||
/>
|
||||
<MetricRow
|
||||
icon={
|
||||
trendUp ? (
|
||||
<TrendingUp className="h-4 w-4 text-red-500" />
|
||||
) : (
|
||||
<TrendingDown className="h-4 w-4 text-green-500" />
|
||||
)
|
||||
}
|
||||
label="Trend vs prior period"
|
||||
value={summary ? (trendUp ? "+" : "") + summary.trend_pct.toFixed(1) + "%" : "—"}
|
||||
sub={
|
||||
summary ? (
|
||||
<span className={"text-xs " + (trendUp ? "text-red-500" : "text-green-500")}>
|
||||
{trendUp ? "▲" : "▼"}
|
||||
</span>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
<MetricRow
|
||||
icon={<Activity className="h-4 w-4 text-blue-500" />}
|
||||
label="Period"
|
||||
value={summary?.period ?? "—"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import type { AgentUsageRow } from "@/types";
|
||||
|
||||
interface AgentUsageChartProps {
|
||||
data: AgentUsageRow[] | undefined;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
function fmtK(n: number): string {
|
||||
if (n >= 1_000) return (n / 1_000).toFixed(0) + "k";
|
||||
return String(n);
|
||||
}
|
||||
|
||||
export function AgentUsageChart({ data, isLoading }: AgentUsageChartProps) {
|
||||
const chartData = [...(data ?? [])]
|
||||
.sort((a, b) => b.total_tokens - a.total_tokens)
|
||||
.slice(0, 10)
|
||||
.map((row) => ({
|
||||
name: row.agent_slug,
|
||||
Tokens: row.total_tokens,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">Agent Tokens Today</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-52 w-full" />
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={208}>
|
||||
<BarChart
|
||||
data={chartData}
|
||||
margin={{ top: 4, right: 8, left: 0, bottom: 24 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" className="opacity-20" />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tick={{ fontSize: 10 }}
|
||||
angle={-30}
|
||||
textAnchor="end"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
tickFormatter={fmtK}
|
||||
tick={{ fontSize: 10 }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
width={36}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value) => [
|
||||
fmtK(typeof value === "number" ? value : 0),
|
||||
"Tokens",
|
||||
]}
|
||||
contentStyle={{ fontSize: 12 }}
|
||||
/>
|
||||
<Bar dataKey="Tokens" fill="var(--chart-1)" radius={[3, 3, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { UsageTimeSeriesChart } from "./usage-time-series-chart";
|
||||
export { ModelUsageDonut } from "./model-usage-donut";
|
||||
export { AgentUsageChart } from "./agent-usage-chart";
|
||||
export { TeamUsageChart } from "./team-usage-chart";
|
||||
export { SessionsTable } from "./sessions-table";
|
||||
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
Legend,
|
||||
} from "recharts";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import type { ModelUsageSlice } from "@/types";
|
||||
|
||||
const CHART_COLORS = [
|
||||
"var(--chart-1)",
|
||||
"var(--chart-2)",
|
||||
"var(--chart-3)",
|
||||
"var(--chart-4)",
|
||||
"var(--chart-5)",
|
||||
];
|
||||
|
||||
interface ModelUsageDonutProps {
|
||||
data: ModelUsageSlice[] | undefined;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function ModelUsageDonut({ data, isLoading }: ModelUsageDonutProps) {
|
||||
const chartData = (data ?? []).map((s) => ({
|
||||
name: s.model,
|
||||
value: s.total_tokens,
|
||||
cost: s.cost_usd,
|
||||
pct: s.pct_of_total,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">By Model</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-52 w-full" />
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={208}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={chartData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={52}
|
||||
outerRadius={80}
|
||||
dataKey="value"
|
||||
paddingAngle={3}
|
||||
>
|
||||
{chartData.map((_, idx) => (
|
||||
<Cell
|
||||
key={idx}
|
||||
fill={CHART_COLORS[idx % CHART_COLORS.length]}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
formatter={(value, name) => [
|
||||
(typeof value === "number" ? value : 0).toLocaleString() +
|
||||
" tokens",
|
||||
name,
|
||||
]}
|
||||
contentStyle={{ fontSize: 12 }}
|
||||
/>
|
||||
<Legend wrapperStyle={{ fontSize: 11 }} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ChevronUp, ChevronDown } from "lucide-react";
|
||||
import type { UsageSession } from "@/types";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
type SortKey = keyof Pick<
|
||||
UsageSession,
|
||||
| "agent_slug"
|
||||
| "started_at"
|
||||
| "total_tokens"
|
||||
| "tokens_input"
|
||||
| "tokens_output"
|
||||
| "tokens_cache"
|
||||
| "cost"
|
||||
| "model"
|
||||
>;
|
||||
|
||||
type SortDir = "asc" | "desc";
|
||||
|
||||
interface Column {
|
||||
key: SortKey;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const COLUMNS: Column[] = [
|
||||
{ key: "agent_slug", label: "Agent" },
|
||||
{ key: "model", label: "Model" },
|
||||
{ key: "started_at", label: "Started" },
|
||||
{ key: "total_tokens", label: "Total" },
|
||||
{ key: "tokens_input", label: "Input" },
|
||||
{ key: "tokens_output", label: "Output" },
|
||||
{ key: "tokens_cache", label: "Cache" },
|
||||
{ key: "cost", label: "Cost" },
|
||||
];
|
||||
|
||||
function formatTime(ts: string): string {
|
||||
return new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
|
||||
function fmtK(n: number): string {
|
||||
if (n >= 1_000) return (n / 1_000).toFixed(1) + "k";
|
||||
return String(n);
|
||||
}
|
||||
|
||||
interface SessionsTableProps {
|
||||
data: UsageSession[] | undefined;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function SessionsTable({ data, isLoading }: SessionsTableProps) {
|
||||
const [sortKey, setSortKey] = useState<SortKey>("started_at");
|
||||
const [sortDir, setSortDir] = useState<SortDir>("desc");
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
const rows = [...(data ?? [])];
|
||||
rows.sort((a, b) => {
|
||||
const av = a[sortKey];
|
||||
const bv = b[sortKey];
|
||||
const cmp =
|
||||
typeof av === "number" && typeof bv === "number"
|
||||
? av - bv
|
||||
: String(av).localeCompare(String(bv));
|
||||
return sortDir === "asc" ? cmp : -cmp;
|
||||
});
|
||||
return rows;
|
||||
}, [data, sortKey, sortDir]);
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(sorted.length / PAGE_SIZE));
|
||||
const visible = sorted.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
|
||||
|
||||
function toggleSort(key: SortKey) {
|
||||
if (sortKey === key) {
|
||||
setSortDir((d) => (d === "asc" ? "desc" : "asc"));
|
||||
} else {
|
||||
setSortKey(key);
|
||||
setSortDir("desc");
|
||||
}
|
||||
setPage(0);
|
||||
}
|
||||
|
||||
function SortIcon({ col }: { col: SortKey }) {
|
||||
if (sortKey !== col) return <ChevronUp className="h-3 w-3 opacity-30 ml-1 inline" />;
|
||||
return sortDir === "asc" ? (
|
||||
<ChevronUp className="h-3 w-3 ml-1 inline" />
|
||||
) : (
|
||||
<ChevronDown className="h-3 w-3 ml-1 inline" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">Recent Sessions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: PAGE_SIZE }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-8" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{COLUMNS.map((col) => (
|
||||
<TableHead
|
||||
key={col.key}
|
||||
className="cursor-pointer select-none text-xs whitespace-nowrap"
|
||||
onClick={() => toggleSort(col.key)}
|
||||
>
|
||||
{col.label}
|
||||
<SortIcon col={col.key} />
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{visible.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={COLUMNS.length} className="text-center text-muted-foreground text-sm py-8">
|
||||
No sessions recorded yet
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
visible.map((s) => (
|
||||
<TableRow key={s.id}>
|
||||
<TableCell className="text-xs font-medium">{s.agent_slug}</TableCell>
|
||||
<TableCell className="text-xs">{s.model}</TableCell>
|
||||
<TableCell className="text-xs">{formatTime(s.started_at)}</TableCell>
|
||||
<TableCell className="text-xs">{fmtK(s.total_tokens)}</TableCell>
|
||||
<TableCell className="text-xs">{fmtK(s.tokens_input)}</TableCell>
|
||||
<TableCell className="text-xs">{fmtK(s.tokens_output)}</TableCell>
|
||||
<TableCell className="text-xs">{fmtK(s.tokens_cache)}</TableCell>
|
||||
<TableCell className="text-xs">${s.cost.toFixed(4)}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="flex items-center justify-between mt-3 pt-3 border-t text-sm">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{sorted.length === 0
|
||||
? "No sessions"
|
||||
: `${page * PAGE_SIZE + 1}–${Math.min((page + 1) * PAGE_SIZE, sorted.length)} of ${sorted.length}`}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
||||
disabled={page === 0}
|
||||
>
|
||||
Prev
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
|
||||
disabled={page >= totalPages - 1}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import type { TeamUsageRow } from "@/types";
|
||||
|
||||
interface TeamUsageChartProps {
|
||||
data: TeamUsageRow[] | undefined;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
function fmtK(n: number): string {
|
||||
if (n >= 1_000) return (n / 1_000).toFixed(0) + "k";
|
||||
return String(n);
|
||||
}
|
||||
|
||||
export function TeamUsageChart({ data, isLoading }: TeamUsageChartProps) {
|
||||
const chartData = [...(data ?? [])]
|
||||
.sort((a, b) => b.total_tokens - a.total_tokens)
|
||||
.map((row) => ({
|
||||
name: row.team.replace(/_/g, " "),
|
||||
Tokens: row.total_tokens,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">Team Tokens</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-52 w-full" />
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={208}>
|
||||
<BarChart
|
||||
data={chartData}
|
||||
margin={{ top: 4, right: 8, left: 0, bottom: 8 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" className="opacity-20" />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tick={{ fontSize: 11 }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
tickFormatter={fmtK}
|
||||
tick={{ fontSize: 10 }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
width={36}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value) => [
|
||||
fmtK(typeof value === "number" ? value : 0),
|
||||
"Tokens",
|
||||
]}
|
||||
contentStyle={{ fontSize: 12 }}
|
||||
/>
|
||||
<Bar dataKey="Tokens" fill="var(--chart-2)" radius={[3, 3, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import type { UsageTimePoint } from "@/types";
|
||||
|
||||
interface UsageTimeSeriesChartProps {
|
||||
data: UsageTimePoint[] | undefined;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
function formatBucket(bucket: string): string {
|
||||
const d = new Date(bucket);
|
||||
// If the bucket has a non-zero time component it is an hourly bucket → show HH:00.
|
||||
// Otherwise it is a daily bucket → show MM/DD.
|
||||
const isHourly = d.getMinutes() === 0 && (d.getHours() !== 0 || bucket.includes("T"));
|
||||
if (isHourly && d.getSeconds() === 0 && !bucket.endsWith("T00:00:00.000Z")) {
|
||||
return d.getHours().toString().padStart(2, "0") + ":00";
|
||||
}
|
||||
return (d.getMonth() + 1) + "/" + d.getDate();
|
||||
}
|
||||
|
||||
function fmtK(n: number): string {
|
||||
if (n >= 1_000) return (n / 1_000).toFixed(0) + "k";
|
||||
return String(n);
|
||||
}
|
||||
|
||||
export function UsageTimeSeriesChart({ data, isLoading }: UsageTimeSeriesChartProps) {
|
||||
const chartData = (data ?? []).map((p) => ({
|
||||
hour: formatBucket(p.bucket),
|
||||
Input: p.tokens_input,
|
||||
Output: p.tokens_output,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">Token Usage Over Time</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-52 w-full" />
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={208}>
|
||||
<AreaChart
|
||||
data={chartData}
|
||||
margin={{ top: 4, right: 8, left: 0, bottom: 0 }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="fillInput" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--chart-1)" stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor="var(--chart-1)" stopOpacity={0.1} />
|
||||
</linearGradient>
|
||||
<linearGradient id="fillOutput" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--chart-2)" stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor="var(--chart-2)" stopOpacity={0.1} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" className="opacity-20" />
|
||||
<XAxis
|
||||
dataKey="hour"
|
||||
tick={{ fontSize: 10 }}
|
||||
interval={3}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
tickFormatter={fmtK}
|
||||
tick={{ fontSize: 10 }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
width={36}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value, name) => [
|
||||
fmtK(typeof value === "number" ? value : 0),
|
||||
name,
|
||||
]}
|
||||
contentStyle={{ fontSize: 12 }}
|
||||
/>
|
||||
<Legend wrapperStyle={{ fontSize: 12 }} />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="Input"
|
||||
stackId="1"
|
||||
stroke="var(--chart-1)"
|
||||
fill="url(#fillInput)"
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="Output"
|
||||
stackId="1"
|
||||
stroke="var(--chart-2)"
|
||||
fill="url(#fillOutput)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -33,3 +33,4 @@ export * from "./use-websocket";
|
||||
export * from "./use-journals";
|
||||
export * from "./use-projects";
|
||||
export * from "./use-work-sessions";
|
||||
export * from "./use-usage";
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { usageApi } from "@/lib/api/usage";
|
||||
import type { UsagePeriod } from "@/lib/api/usage";
|
||||
import type {
|
||||
UsageSummary,
|
||||
AgentUsageRow,
|
||||
TeamUsageRow,
|
||||
ModelUsageSlice,
|
||||
UsageTimePoint,
|
||||
UsageProjection,
|
||||
CacheEfficiencyResponse,
|
||||
UsageSession,
|
||||
} from "@/types";
|
||||
|
||||
// =============================================================================
|
||||
// QUERY KEYS
|
||||
// =============================================================================
|
||||
|
||||
export const usageKeys = {
|
||||
all: ["usage"] as const,
|
||||
summary: (period: UsagePeriod) => [...usageKeys.all, "summary", period] as const,
|
||||
timeSeries: (period: UsagePeriod) => [...usageKeys.all, "time-series", period] as const,
|
||||
agentUsage: (period: UsagePeriod) => [...usageKeys.all, "by-agent", period] as const,
|
||||
teamUsage: (period: UsagePeriod) => [...usageKeys.all, "by-team", period] as const,
|
||||
modelUsage: (period: UsagePeriod) => [...usageKeys.all, "by-model", period] as const,
|
||||
projection: () => [...usageKeys.all, "projection"] as const,
|
||||
cacheEfficiency: (period: UsagePeriod) => [...usageKeys.all, "cache-efficiency", period] as const,
|
||||
sessions: (limit: number) => [...usageKeys.all, "sessions", limit] as const,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// HOOKS
|
||||
// =============================================================================
|
||||
|
||||
/** Aggregated usage summary (tokens_input, tokens_output, total_cost_usd, …) */
|
||||
export function useUsageSummary(period: UsagePeriod = "24h") {
|
||||
return useQuery<UsageSummary>({
|
||||
queryKey: usageKeys.summary(period),
|
||||
queryFn: () => usageApi.getUsageSummary(period),
|
||||
refetchInterval: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
/** Bucketed time-series data for the stacked area chart */
|
||||
export function useUsageTimeSeries(period: UsagePeriod = "24h") {
|
||||
return useQuery<UsageTimePoint[]>({
|
||||
queryKey: usageKeys.timeSeries(period),
|
||||
queryFn: () => usageApi.getUsageTimeSeries(period),
|
||||
refetchInterval: 120_000,
|
||||
});
|
||||
}
|
||||
|
||||
/** Per-agent usage rows for bar chart and agent card mini-bars */
|
||||
export function useAgentUsage(period: UsagePeriod = "24h") {
|
||||
return useQuery<AgentUsageRow[]>({
|
||||
queryKey: usageKeys.agentUsage(period),
|
||||
queryFn: () => usageApi.getAgentUsage(period),
|
||||
refetchInterval: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
/** Per-team usage rows from the dedicated by-team endpoint */
|
||||
export function useTeamUsage(period: UsagePeriod = "24h") {
|
||||
return useQuery<TeamUsageRow[]>({
|
||||
queryKey: usageKeys.teamUsage(period),
|
||||
queryFn: () => usageApi.getTeamUsage(period),
|
||||
refetchInterval: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
/** Per-model slices for the donut chart */
|
||||
export function useModelUsage(period: UsagePeriod = "24h") {
|
||||
return useQuery<ModelUsageSlice[]>({
|
||||
queryKey: usageKeys.modelUsage(period),
|
||||
queryFn: () => usageApi.getModelUsage(period),
|
||||
refetchInterval: 120_000,
|
||||
});
|
||||
}
|
||||
|
||||
/** Monthly cost projection based on 7-day rolling average */
|
||||
export function useUsageProjection() {
|
||||
return useQuery<UsageProjection>({
|
||||
queryKey: usageKeys.projection(),
|
||||
queryFn: () => usageApi.getUsageProjection(),
|
||||
refetchInterval: 300_000,
|
||||
});
|
||||
}
|
||||
|
||||
/** Cache efficiency stats */
|
||||
export function useCacheEfficiency(period: UsagePeriod = "24h") {
|
||||
return useQuery<CacheEfficiencyResponse>({
|
||||
queryKey: usageKeys.cacheEfficiency(period),
|
||||
queryFn: () => usageApi.getCacheEfficiency(period),
|
||||
refetchInterval: 120_000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Recent inference sessions — mock-mode only.
|
||||
*
|
||||
* Returns an empty array in production (no real backend endpoint for sessions).
|
||||
* The SessionsTable will display "No sessions recorded yet" gracefully.
|
||||
*/
|
||||
export function useUsageSessions(limit: number = 100) {
|
||||
return useQuery<UsageSession[]>({
|
||||
queryKey: usageKeys.sessions(limit),
|
||||
queryFn: () => usageApi.getUsageSessions(limit),
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export { api, API_URL } from "./client";
|
||||
export { usageApi } from "./usage";
|
||||
export { tasksApi } from "./tasks";
|
||||
export { orchestratorApi } from "./orchestrator";
|
||||
export { channelsApi } from "./channels";
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
import api from "./client";
|
||||
import { isMockMode } from "@/lib/mock-data";
|
||||
import type {
|
||||
UsageSummary,
|
||||
AgentUsageRow,
|
||||
TeamUsageRow,
|
||||
ModelUsageSlice,
|
||||
UsageTimePoint,
|
||||
UsageProjection,
|
||||
CacheEfficiencyResponse,
|
||||
UsageSession,
|
||||
} from "@/types";
|
||||
|
||||
export type UsagePeriod = "24h" | "7d" | "30d";
|
||||
|
||||
// =============================================================================
|
||||
// MOCK DATA — shapes must exactly match the real backend response schemas
|
||||
// =============================================================================
|
||||
|
||||
function mockSummary(period: UsagePeriod = "24h"): UsageSummary {
|
||||
const scale = period === "30d" ? 30 : period === "7d" ? 7 : 1;
|
||||
const base = 124_800 * scale;
|
||||
return {
|
||||
tokens_input: Math.round(base * 0.55),
|
||||
tokens_output: Math.round(base * 0.35),
|
||||
total_tokens: base,
|
||||
total_cost_usd: parseFloat((base * 0.000030).toFixed(6)),
|
||||
trend_pct: 12.5,
|
||||
period,
|
||||
};
|
||||
}
|
||||
|
||||
function mockTimeSeries(period: UsagePeriod = "24h"): UsageTimePoint[] {
|
||||
const now = new Date();
|
||||
const points = period === "24h" ? 24 : period === "7d" ? 7 : 30;
|
||||
const step = period === "24h" ? "hour" : "day";
|
||||
return Array.from({ length: points }, (_, i) => {
|
||||
const ts = new Date(now);
|
||||
if (step === "hour") {
|
||||
ts.setHours(now.getHours() - (points - 1 - i), 0, 0, 0);
|
||||
} else {
|
||||
ts.setDate(now.getDate() - (points - 1 - i));
|
||||
ts.setHours(0, 0, 0, 0);
|
||||
}
|
||||
const base = 3_000 + Math.round(Math.random() * 4_000);
|
||||
const tokens_input = Math.round(base * 0.55);
|
||||
const tokens_output = Math.round(base * 0.35);
|
||||
const total_tokens = base;
|
||||
return {
|
||||
bucket: ts.toISOString(),
|
||||
tokens_input,
|
||||
tokens_output,
|
||||
total_tokens,
|
||||
cost_usd: parseFloat((total_tokens * 0.000030).toFixed(6)),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function mockAgentUsage(period: UsagePeriod = "24h"): AgentUsageRow[] {
|
||||
const scale = period === "30d" ? 30 : period === "7d" ? 7 : 1;
|
||||
const agents = [
|
||||
{ agent_slug: "be-dev-1" },
|
||||
{ agent_slug: "be-dev-2" },
|
||||
{ agent_slug: "fe-dev-1" },
|
||||
{ agent_slug: "fe-dev-2" },
|
||||
{ agent_slug: "ux-dev-1" },
|
||||
{ agent_slug: "be-qa" },
|
||||
{ agent_slug: "fe-qa" },
|
||||
{ agent_slug: "main-pm" },
|
||||
];
|
||||
const grand = agents.length * 15_000 * scale;
|
||||
return agents.map((a) => {
|
||||
const ti = Math.round((5_000 + Math.random() * 20_000) * scale);
|
||||
const to_ = Math.round(ti * 0.65);
|
||||
const total = ti + to_;
|
||||
return {
|
||||
agent_slug: a.agent_slug,
|
||||
tokens_input: ti,
|
||||
tokens_output: to_,
|
||||
total_tokens: total,
|
||||
cost_usd: parseFloat((total * 0.000030).toFixed(6)),
|
||||
pct_of_total: parseFloat(((total / grand) * 100).toFixed(2)),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function mockTeamUsage(period: UsagePeriod = "24h"): TeamUsageRow[] {
|
||||
const scale = period === "30d" ? 30 : period === "7d" ? 7 : 1;
|
||||
const teams = ["backend", "frontend", "ux_ui", "main_pm"];
|
||||
const grand = teams.length * 50_000 * scale;
|
||||
return teams.map((team) => {
|
||||
const ti = Math.round((30_000 + Math.random() * 40_000) * scale);
|
||||
const to_ = Math.round(ti * 0.65);
|
||||
const total = ti + to_;
|
||||
return {
|
||||
team,
|
||||
tokens_input: ti,
|
||||
tokens_output: to_,
|
||||
total_tokens: total,
|
||||
cost_usd: parseFloat((total * 0.000030).toFixed(6)),
|
||||
pct_of_total: parseFloat(((total / grand) * 100).toFixed(2)),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function mockModelUsage(period: UsagePeriod = "24h"): ModelUsageSlice[] {
|
||||
const scale = period === "30d" ? 30 : period === "7d" ? 7 : 1;
|
||||
const models = [
|
||||
{ model: "claude-opus-4", share: 0.548 },
|
||||
{ model: "claude-sonnet-4", share: 0.346 },
|
||||
{ model: "claude-haiku-4", share: 0.106 },
|
||||
];
|
||||
const base = 124_800 * scale;
|
||||
return models.map((m) => {
|
||||
const ti = Math.round(base * m.share * 0.55);
|
||||
const to_ = Math.round(base * m.share * 0.35);
|
||||
const total = Math.round(base * m.share);
|
||||
return {
|
||||
model: m.model,
|
||||
tokens_input: ti,
|
||||
tokens_output: to_,
|
||||
total_tokens: total,
|
||||
cost_usd: parseFloat((total * 0.000030).toFixed(6)),
|
||||
pct_of_total: parseFloat((m.share * 100).toFixed(1)),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function mockProjection(): UsageProjection {
|
||||
const total_cost_7d = parseFloat((124_800 * 7 * 0.000030).toFixed(6));
|
||||
return {
|
||||
total_cost_7d,
|
||||
avg_daily_cost_usd: parseFloat((total_cost_7d / 7).toFixed(6)),
|
||||
projected_monthly_cost_usd: parseFloat((total_cost_7d / 7 * 30).toFixed(4)),
|
||||
basis_days: 7,
|
||||
};
|
||||
}
|
||||
|
||||
function mockCacheEfficiency(period: UsagePeriod = "24h"): CacheEfficiencyResponse {
|
||||
return {
|
||||
cache_hit_rate: 0.3142,
|
||||
tokens_cache_read: 39_168,
|
||||
tokens_cache_write: 12_480,
|
||||
tokens_input: 85_632,
|
||||
cost_saved_by_cache_usd: parseFloat((39_168 * (3.00 - 0.30) / 1_000_000).toFixed(6)),
|
||||
period,
|
||||
};
|
||||
}
|
||||
|
||||
function mockSessions(): UsageSession[] {
|
||||
const models = ["claude-opus-4", "claude-sonnet-4", "claude-haiku-4"];
|
||||
const agentSlugs = ["be-dev-1", "be-dev-2", "fe-dev-1", "fe-qa", "main-pm"];
|
||||
return Array.from({ length: 35 }, (_, i) => {
|
||||
const agent_slug = agentSlugs[i % agentSlugs.length];
|
||||
const model = models[i % models.length];
|
||||
const input = Math.round(2_000 + Math.random() * 8_000);
|
||||
const output = Math.round(500 + Math.random() * 3_000);
|
||||
const cache = Math.round(100 + Math.random() * 1_000);
|
||||
const started = new Date(Date.now() - (i + 1) * 12 * 60_000);
|
||||
const ended = i < 3 ? null : new Date(started.getTime() + Math.round(5 + Math.random() * 55) * 60_000);
|
||||
return {
|
||||
id: `session-mock-${i + 1}`,
|
||||
agent_slug,
|
||||
started_at: started.toISOString(),
|
||||
ended_at: ended ? ended.toISOString() : null,
|
||||
tokens_input: input,
|
||||
tokens_output: output,
|
||||
tokens_cache: cache,
|
||||
total_tokens: input + output + cache,
|
||||
cost: parseFloat(((input + output) * 0.00003 + cache * 0.000003).toFixed(4)),
|
||||
model,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// API OBJECT
|
||||
// =============================================================================
|
||||
|
||||
export const usageApi = {
|
||||
/** Aggregated token usage summary — GET /usage/summary?period= */
|
||||
getUsageSummary: async (period: UsagePeriod = "24h"): Promise<UsageSummary> => {
|
||||
if (isMockMode()) return mockSummary(period);
|
||||
const { data } = await api.get<UsageSummary>("/usage/summary", {
|
||||
params: { period },
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
/** Bucketed time-series — GET /usage/time-series?period= */
|
||||
getUsageTimeSeries: async (period: UsagePeriod = "24h"): Promise<UsageTimePoint[]> => {
|
||||
if (isMockMode()) return mockTimeSeries(period);
|
||||
const { data } = await api.get<UsageTimePoint[]>("/usage/time-series", {
|
||||
params: { period },
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
/** Per-agent usage rows — GET /usage/by-agent?period= */
|
||||
getAgentUsage: async (period: UsagePeriod = "24h"): Promise<AgentUsageRow[]> => {
|
||||
if (isMockMode()) return mockAgentUsage(period);
|
||||
const { data } = await api.get<AgentUsageRow[]>("/usage/by-agent", {
|
||||
params: { period },
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
/** Per-team usage rows — GET /usage/by-team?period= */
|
||||
getTeamUsage: async (period: UsagePeriod = "24h"): Promise<TeamUsageRow[]> => {
|
||||
if (isMockMode()) return mockTeamUsage(period);
|
||||
const { data } = await api.get<TeamUsageRow[]>("/usage/by-team", {
|
||||
params: { period },
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
/** Per-model usage slices — GET /usage/by-model?period= */
|
||||
getModelUsage: async (period: UsagePeriod = "24h"): Promise<ModelUsageSlice[]> => {
|
||||
if (isMockMode()) return mockModelUsage(period);
|
||||
const { data } = await api.get<ModelUsageSlice[]>("/usage/by-model", {
|
||||
params: { period },
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
/** Monthly cost projection — GET /usage/projection */
|
||||
getUsageProjection: async (): Promise<UsageProjection> => {
|
||||
if (isMockMode()) return mockProjection();
|
||||
const { data } = await api.get<UsageProjection>("/usage/projection");
|
||||
return data;
|
||||
},
|
||||
|
||||
/** Cache efficiency stats — GET /usage/cache-efficiency?period= */
|
||||
getCacheEfficiency: async (period: UsagePeriod = "24h"): Promise<CacheEfficiencyResponse> => {
|
||||
if (isMockMode()) return mockCacheEfficiency(period);
|
||||
const { data } = await api.get<CacheEfficiencyResponse>("/usage/cache-efficiency", {
|
||||
params: { period },
|
||||
});
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Recent inference sessions — mock-mode only.
|
||||
*
|
||||
* The backend has no /usage/sessions endpoint. In production this
|
||||
* returns an empty array so SessionsTable shows a graceful "no data"
|
||||
* state instead of throwing a 404.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
getUsageSessions: async (_limit: number = 100): Promise<UsageSession[]> => {
|
||||
if (isMockMode()) return mockSessions();
|
||||
return [];
|
||||
},
|
||||
};
|
||||
@@ -1253,3 +1253,92 @@ export interface CEOApprovalRequest {
|
||||
export interface CEORejectRequest {
|
||||
notes: string; // Required for rejection
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TOKEN USAGE TYPES (aligned to real backend: GET /api/usage/*)
|
||||
// =============================================================================
|
||||
|
||||
/** Aggregated token and cost totals — GET /usage/summary?period=24h|7d|30d */
|
||||
export interface UsageSummary {
|
||||
tokens_input: number;
|
||||
tokens_output: number;
|
||||
total_tokens: number;
|
||||
total_cost_usd: number;
|
||||
trend_pct: number;
|
||||
period: string;
|
||||
}
|
||||
|
||||
/** Per-agent usage row — GET /usage/by-agent?period=24h|7d|30d */
|
||||
export interface AgentUsageRow {
|
||||
agent_slug: string;
|
||||
tokens_input: number;
|
||||
tokens_output: number;
|
||||
total_tokens: number;
|
||||
cost_usd: number;
|
||||
pct_of_total: number;
|
||||
}
|
||||
|
||||
/** Per-team usage row — GET /usage/by-team?period=24h|7d|30d */
|
||||
export interface TeamUsageRow {
|
||||
team: string;
|
||||
tokens_input: number;
|
||||
tokens_output: number;
|
||||
total_tokens: number;
|
||||
cost_usd: number;
|
||||
pct_of_total: number;
|
||||
}
|
||||
|
||||
/** Per-model usage slice — GET /usage/by-model?period=24h|7d|30d */
|
||||
export interface ModelUsageSlice {
|
||||
model: string;
|
||||
tokens_input: number;
|
||||
tokens_output: number;
|
||||
total_tokens: number;
|
||||
cost_usd: number;
|
||||
pct_of_total: number;
|
||||
}
|
||||
|
||||
/** One data point in a token-usage time series — GET /usage/time-series?period=24h|7d|30d
|
||||
*
|
||||
* - 24h → hourly buckets; 7d / 30d → daily buckets
|
||||
* - bucket is an ISO datetime string (from PostgreSQL date_trunc)
|
||||
*/
|
||||
export interface UsageTimePoint {
|
||||
bucket: string;
|
||||
tokens_input: number;
|
||||
tokens_output: number;
|
||||
total_tokens: number;
|
||||
cost_usd: number;
|
||||
}
|
||||
|
||||
/** Monthly cost projection — GET /usage/projection */
|
||||
export interface UsageProjection {
|
||||
total_cost_7d: number;
|
||||
avg_daily_cost_usd: number;
|
||||
projected_monthly_cost_usd: number;
|
||||
basis_days: number;
|
||||
}
|
||||
|
||||
/** Cache efficiency stats — GET /usage/cache-efficiency?period=24h|7d|30d */
|
||||
export interface CacheEfficiencyResponse {
|
||||
cache_hit_rate: number;
|
||||
tokens_cache_read: number;
|
||||
tokens_cache_write: number;
|
||||
tokens_input: number;
|
||||
cost_saved_by_cache_usd: number;
|
||||
period: string;
|
||||
}
|
||||
|
||||
/** Individual inference session for the sessions table (mock-mode only — no real backend endpoint) */
|
||||
export interface UsageSession {
|
||||
id: string;
|
||||
agent_slug: string;
|
||||
started_at: string;
|
||||
ended_at: string | null;
|
||||
tokens_input: number;
|
||||
tokens_output: number;
|
||||
tokens_cache: number;
|
||||
total_tokens: number;
|
||||
cost: number;
|
||||
model: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user