From 446adef0276532176da9a1d5af2861d47a35e5c0 Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Tue, 2 Jun 2026 06:01:20 +0300 Subject: [PATCH] =?UTF-8?q?feat(069-B1):=20Dashboard=20Overview=E2=86=94Us?= =?UTF-8?q?age=20switcher=20+=20usage=20API=20client?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spec 069 Stream B1 (T016–T017). Adds the Overview↔Usage switcher to the dashboard and the typed client for the A3 GET /api/v1/activity/usage endpoint (#565). - api: getActivityUsage(params) forwards only supplied filters (window/server/tool/status/top/sort); UsageAggregateResponse + UsageToolStat/UsageOtherBucket/UsageTimeBucket types mirror the contract. - Dashboard: tabbed Overview↔Usage switcher rendered with v-show (never v-if) so Overview state survives a switch-back (SC-006). Usage panel has a 24h/7d/all window selector, tokens-saved headline (FR-007), loading/error/empty states (FR-009) and a baseline per-tool rollup table; the rich charts arrive in B2 (T018–T022). - Lazy load: the aggregate is fetched on first Usage activation and on window change only, so the Overview first paint is never blocked (SC-004). - Tests (TDD): activity-usage.spec (client param forwarding) + dashboard-usage-switcher.spec (default tab, lazy fetch, v-show preserve, window refetch). All 91 frontend unit tests green; vue-tsc clean. Related #745 --- frontend/src/services/api.ts | 17 +- frontend/src/types/api.ts | 62 ++++++ frontend/src/views/Dashboard.vue | 190 +++++++++++++++++- frontend/tests/unit/activity-usage.spec.ts | 98 +++++++++ .../unit/dashboard-usage-switcher.spec.ts | 148 ++++++++++++++ specs/069-observability-usage-graphs/tasks.md | 4 +- 6 files changed, 515 insertions(+), 4 deletions(-) create mode 100644 frontend/tests/unit/activity-usage.spec.ts create mode 100644 frontend/tests/unit/dashboard-usage-switcher.spec.ts diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 0fc3370b..1a5ba58d 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, UsageQueryParams, UsageAggregateResponse } from '@/types' // Event types for API service export interface APIAuthEvent { @@ -758,6 +758,21 @@ class APIService { return this.request(`/api/v1/activity/summary?period=${period}`) } + // Spec 069 (T017): actor-owned usage aggregate backing the Usage panel. + // Only the supplied params are forwarded; unset filters are omitted so the + // daemon applies its documented defaults (window=24h, sort=resp_bytes, top=20). + async getActivityUsage(params: UsageQueryParams = {}): Promise> { + const searchParams = new URLSearchParams() + if (params.window) searchParams.append('window', params.window) + if (params.server) searchParams.append('server', params.server) + if (params.tool) searchParams.append('tool', params.tool) + if (params.status) searchParams.append('status', params.status) + if (params.top !== undefined) searchParams.append('top', String(params.top)) + if (params.sort) searchParams.append('sort', params.sort) + 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 6a341f46..4530d9e6 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -631,6 +631,68 @@ export interface ActivitySummaryResponse { end_time: string } +// Usage aggregate types (Spec 069 — GET /api/v1/activity/usage) + +export type UsageWindow = '24h' | '7d' | 'all' +export type UsageSort = 'calls' | 'resp_bytes' | 'error_rate' | 'p95' +export type UsageStatus = 'success' | 'error' | 'blocked' + +export interface UsageQueryParams { + window?: UsageWindow + server?: string + tool?: string + status?: UsageStatus + top?: number + sort?: UsageSort +} + +// Per-(server,tool) rollup row. `avg_resp_bytes`/`avg_req_bytes` are null when +// there are no sized (non-zero-byte) calls. `blocked` counts policy-prevented +// attempts that never executed (excluded from `calls`, latency and bytes). +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 + total_req_bytes: number + avg_req_bytes: number | null + sized_calls: number + p50_ms: number + p95_ms: number + last_used: string +} + +// Present only when the tool list was truncated to `top`. +export interface UsageOtherBucket { + tools_folded: number + calls: number + total_resp_bytes: number +} + +// One timeline bar (executed calls only; blocked attempts excluded). +export interface UsageTimeBucket { + start: string + calls: number + errors: number + total_resp_bytes: number +} + +export interface UsageAggregateResponse { + window: UsageWindow + generated_at: string + freshness_ms: number + token_source: string + tokens_saved: number + tokens_saved_percentage: number + tools: UsageToolStat[] + other?: UsageOtherBucket + timeline: UsageTimeBucket[] +} + // Agent Token types (Spec 028) export interface AgentTokenInfo { diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue index 379674ed..875595bf 100644 --- a/frontend/src/views/Dashboard.vue +++ b/frontend/src/views/Dashboard.vue @@ -1,5 +1,35 @@