From dc8966c403ed697307bd4fe9e11b762fafd60fc6 Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Tue, 2 Jun 2026 06:06:16 +0300 Subject: [PATCH] feat(069-B2): usage charts + Usage.vue + Dashboard switcher Spec 069 Stream B2 (frontend). Surfaces the activity-log usage aggregate (GET /api/v1/activity/usage, A3) as Web UI graphs. - usage/CallHistogram.vue, ResponseSizeRanking.vue (size-based token-sink proxy, FR-006), ErrorRateChart.vue (+ per-tool p50/p95 latency table), Timeline.vue (calls/errors stacked, honors window/filters, FR-008). - views/Usage.vue: tokens-saved headline from ServerTokenMetrics (FR-007), window selector (24h/7d/all), status/sort filters, empty/low-data state (FR-009), 30s auto-refresh against the cached snapshot. - Dashboard.vue: Overview<->Usage switcher (T016); Overview kept via v-show so its state survives a round-trip (SC-006); Usage code-split via defineAsyncComponent so the chart bundle/fetch don't block first paint (SC-004). - api.ts getActivityUsage() (T017) + UsageAggregateResponse types. - utils/usageFormat.ts shared formatters + palette. Covers T016-T023. Verified with a Playwright sweep (7/7, 0 console errors) against populated + empty instances; report kept local per QA-report policy. Related #745 --- .../src/components/usage/CallHistogram.vue | 73 ++++++ .../src/components/usage/ErrorRateChart.vue | 99 ++++++++ .../components/usage/ResponseSizeRanking.vue | 84 +++++++ frontend/src/components/usage/Timeline.vue | 92 ++++++++ frontend/src/services/api.ts | 23 +- frontend/src/types/api.ts | 49 ++++ frontend/src/utils/usageFormat.ts | 47 ++++ frontend/src/views/Dashboard.vue | 49 +++- frontend/src/views/Usage.vue | 214 ++++++++++++++++++ 9 files changed, 728 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/usage/CallHistogram.vue create mode 100644 frontend/src/components/usage/ErrorRateChart.vue create mode 100644 frontend/src/components/usage/ResponseSizeRanking.vue create mode 100644 frontend/src/components/usage/Timeline.vue create mode 100644 frontend/src/utils/usageFormat.ts create mode 100644 frontend/src/views/Usage.vue diff --git a/frontend/src/components/usage/CallHistogram.vue b/frontend/src/components/usage/CallHistogram.vue new file mode 100644 index 000000000..8cb0de74c --- /dev/null +++ b/frontend/src/components/usage/CallHistogram.vue @@ -0,0 +1,73 @@ + + + diff --git a/frontend/src/components/usage/ErrorRateChart.vue b/frontend/src/components/usage/ErrorRateChart.vue new file mode 100644 index 000000000..0637fa0ad --- /dev/null +++ b/frontend/src/components/usage/ErrorRateChart.vue @@ -0,0 +1,99 @@ + + + diff --git a/frontend/src/components/usage/ResponseSizeRanking.vue b/frontend/src/components/usage/ResponseSizeRanking.vue new file mode 100644 index 000000000..88ab8fc42 --- /dev/null +++ b/frontend/src/components/usage/ResponseSizeRanking.vue @@ -0,0 +1,84 @@ + + + diff --git a/frontend/src/components/usage/Timeline.vue b/frontend/src/components/usage/Timeline.vue new file mode 100644 index 000000000..f2e7ccc7d --- /dev/null +++ b/frontend/src/components/usage/Timeline.vue @@ -0,0 +1,92 @@ + + + diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 0fc3370b9..d9173c418 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1,4 +1,4 @@ -import type { APIResponse, Server, Tool, ToolApproval, SearchResult, StatusUpdate, SecretRef, MigrationAnalysis, ConfigSecretsResponse, GetToolCallsResponse, GetToolCallDetailResponse, GetServerToolCallsResponse, GetConfigResponse, ValidateConfigResponse, ConfigApplyResult, ServerTokenMetrics, GetRegistriesResponse, SearchRegistryServersResponse, GetSessionsResponse, GetSessionDetailResponse, InfoResponse, ActivityListResponse, ActivityDetailResponse, ActivitySummaryResponse, ImportResponse, AgentTokenInfo, CreateAgentTokenRequest, CreateAgentTokenResponse, RoutingInfo, ConnectStatusResponse, ConnectResult, OnboardingStateResponse, OnboardingMarkRequest, DiagnosticFixResponse, GlobalToolsResponse } from '@/types' +import type { APIResponse, Server, Tool, ToolApproval, SearchResult, StatusUpdate, SecretRef, MigrationAnalysis, ConfigSecretsResponse, GetToolCallsResponse, GetToolCallDetailResponse, GetServerToolCallsResponse, GetConfigResponse, ValidateConfigResponse, ConfigApplyResult, ServerTokenMetrics, GetRegistriesResponse, SearchRegistryServersResponse, GetSessionsResponse, GetSessionDetailResponse, InfoResponse, ActivityListResponse, ActivityDetailResponse, ActivitySummaryResponse, ImportResponse, AgentTokenInfo, CreateAgentTokenRequest, CreateAgentTokenResponse, RoutingInfo, ConnectStatusResponse, ConnectResult, OnboardingStateResponse, OnboardingMarkRequest, DiagnosticFixResponse, GlobalToolsResponse, UsageAggregateResponse, UsageWindow, UsageSort, UsageStatus } from '@/types' // Event types for API service export interface APIAuthEvent { @@ -758,6 +758,27 @@ class APIService { return this.request(`/api/v1/activity/summary?period=${period}`) } + // Usage statistics aggregate for the Web UI usage graphs (Spec 069). + async getActivityUsage(params?: { + window?: UsageWindow + server?: string + tool?: string + status?: UsageStatus + top?: number + sort?: UsageSort + }): Promise> { + const searchParams = new URLSearchParams() + if (params) { + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== '') { + searchParams.append(key, String(value)) + } + }) + } + const qs = searchParams.toString() + return this.request(`/api/v1/activity/usage${qs ? '?' + qs : ''}`) + } + getActivityExportUrl(params: { format: 'json' | 'csv' type?: string diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 6a341f462..359b5dfe2 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -419,6 +419,55 @@ export interface ServerTokenMetrics { per_server_tool_list_sizes: Record } +// Usage statistics aggregate — GET /api/v1/activity/usage (Spec 069). +// Mirrors contracts.UsageAggregateResponse. Per-tool metrics are +// lifetime-cumulative; `window` scopes the timeline + the tool-list membership. +export interface UsageToolStat { + server: string + tool: string + calls: number + errors: number + error_rate: number + blocked: number + total_resp_bytes: number + avg_resp_bytes: number | null // null when only legacy 0-byte calls exist + total_req_bytes: number + avg_req_bytes: number | null + sized_calls: number + p50_ms: number + p95_ms: number + last_used: string +} + +export interface UsageOtherBucket { + tools_folded: number + calls: number + total_resp_bytes: number +} + +export interface UsageTimeBucket { + start: string + calls: number + errors: number + total_resp_bytes: number +} + +export interface UsageAggregateResponse { + window: string + generated_at: string + freshness_ms: number + token_source: string // "bytes" — size-based proxy (FR-006) + tokens_saved: number // echoed from ServerTokenMetrics (FR-007) + tokens_saved_percentage: number + tools: UsageToolStat[] + other?: UsageOtherBucket | null // present only when list truncated to top-N + timeline: UsageTimeBucket[] +} + +export type UsageWindow = '24h' | '7d' | 'all' +export type UsageSort = 'calls' | 'resp_bytes' | 'error_rate' | 'p95' +export type UsageStatus = 'success' | 'error' | 'blocked' + export interface ToolCallRecord { id: string server_id: string diff --git a/frontend/src/utils/usageFormat.ts b/frontend/src/utils/usageFormat.ts new file mode 100644 index 000000000..79177865a --- /dev/null +++ b/frontend/src/utils/usageFormat.ts @@ -0,0 +1,47 @@ +// Shared formatting helpers + colour palette for the Usage graphs (Spec 069 B2). +// Kept dependency-free so the usage chart components can import them without +// pulling in Dashboard internals. + +/** Compact number: 1234 -> "1.2K", 2_500_000 -> "2.5M". */ +export function formatNumber(num: number): string { + if (!Number.isFinite(num)) return '0' + if (Math.abs(num) >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M` + if (Math.abs(num) >= 1_000) return `${(num / 1_000).toFixed(1)}K` + return String(num) +} + +/** Human byte size: 0 -> "0 B", 2048 -> "2.0 KB". */ +export function formatBytes(bytes: number | null | undefined): string { + if (bytes == null || !Number.isFinite(bytes) || bytes <= 0) return '0 B' + const units = ['B', 'KB', 'MB', 'GB', 'TB'] + let i = 0 + let v = bytes + while (v >= 1024 && i < units.length - 1) { + v /= 1024 + i++ + } + return `${i === 0 ? v : v.toFixed(1)} ${units[i]}` +} + +/** Latency in ms: 0 -> "0 ms", 1500 -> "1.5 s". */ +export function formatLatency(ms: number | null | undefined): string { + if (ms == null || !Number.isFinite(ms) || ms <= 0) return '0 ms' + if (ms >= 1000) return `${(ms / 1000).toFixed(1)} s` + return `${Math.round(ms)} ms` +} + +/** A short, readable label for a (server, tool) pair. */ +export function toolLabel(server: string, tool: string): string { + return `${server}:${tool}` +} + +/** Stable, colour-blind-friendly palette shared across the usage charts. */ +export const USAGE_PALETTE = [ + '#3b82f6', '#10b981', '#f59e0b', '#ec4899', '#8b5cf6', + '#06b6d4', '#ef4444', '#14b8a6', '#f97316', '#a855f7', + '#6366f1', '#84cc16', '#f43f5e', '#0ea5e9', '#22c55e', '#eab308', +] + +export function paletteColor(index: number): string { + return USAGE_PALETTE[index % USAGE_PALETTE.length] +} diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue index 379674ed6..22fac07e5 100644 --- a/frontend/src/views/Dashboard.vue +++ b/frontend/src/views/Dashboard.vue @@ -3,6 +3,38 @@ + +
+ Overview + Usage +
+ + +
+ + + + +
+ + +
+
+ @@ -385,7 +419,7 @@