diff --git a/packages/control-plane-ui/public/locales/en/pages.json b/packages/control-plane-ui/public/locales/en/pages.json index 6176dae3..8a0a7ae3 100644 --- a/packages/control-plane-ui/public/locales/en/pages.json +++ b/packages/control-plane-ui/public/locales/en/pages.json @@ -79,6 +79,14 @@ "sectionLatencyHealthSubtitle": "Our overhead versus the upstream provider — by the busiest provider in the window.", "latencyOurOverheadP95": "Our Overhead P95", "latencyUpstreamTotalP95": "Upstream P95", + "latencyEndToEndP95": "End-to-End P95", + "source_all": "All", + "source_vk": "Virtual keys", + "source_proxy": "Proxy", + "source_agent": "Agent", + "sourcePickerLabel": "Traffic source", + "complianceWarningTitle": "No compliance hooks are active", + "complianceWarningBody": "No traffic in the selected window was covered by compliance hooks. Enable a hook under Compliance → Hooks & Policies to inspect and govern this traffic.", "latencySlowestProvider": "Slowest Upstream Provider" }, "providers": { diff --git a/packages/control-plane-ui/public/locales/es/pages.json b/packages/control-plane-ui/public/locales/es/pages.json index d0e64e7b..19abd57c 100644 --- a/packages/control-plane-ui/public/locales/es/pages.json +++ b/packages/control-plane-ui/public/locales/es/pages.json @@ -79,6 +79,14 @@ "sectionLatencyHealthSubtitle": "Nuestro overhead frente al proveedor upstream — según el proveedor con más tráfico en la ventana.", "latencyOurOverheadP95": "Nuestro overhead P95", "latencyUpstreamTotalP95": "Upstream P95", + "latencyEndToEndP95": "Latencia total P95", + "source_all": "Todo", + "source_vk": "Claves virtuales", + "source_proxy": "Proxy", + "source_agent": "Agente", + "sourcePickerLabel": "Fuente de tráfico", + "complianceWarningTitle": "No hay hooks de cumplimiento activos", + "complianceWarningBody": "Ningún tráfico en el período seleccionado fue cubierto por hooks de cumplimiento. Active un hook en Cumplimiento → Hooks y políticas para inspeccionar y gobernar este tráfico.", "latencySlowestProvider": "Proveedor upstream más lento" }, "providers": { diff --git a/packages/control-plane-ui/public/locales/zh/pages.json b/packages/control-plane-ui/public/locales/zh/pages.json index 9e165cf2..ed30d093 100644 --- a/packages/control-plane-ui/public/locales/zh/pages.json +++ b/packages/control-plane-ui/public/locales/zh/pages.json @@ -79,6 +79,14 @@ "sectionLatencyHealthSubtitle": "对比我方开销与上游提供商耗时 — 取该时间窗口内流量最大的提供商。", "latencyOurOverheadP95": "我方开销 P95", "latencyUpstreamTotalP95": "上游 P95", + "latencyEndToEndP95": "端到端 P95", + "source_all": "全部", + "source_vk": "虚拟密钥", + "source_proxy": "代理", + "source_agent": "代理端", + "sourcePickerLabel": "流量来源", + "complianceWarningTitle": "未启用合规钩子", + "complianceWarningBody": "所选时间窗口内没有流量经过合规钩子处理。请在“合规 → 钩子与策略”中启用钩子,以检查和治理此流量。", "latencySlowestProvider": "最慢的上游提供商" }, "providers": { diff --git a/packages/control-plane-ui/src/i18n/locales/en/pages.json b/packages/control-plane-ui/src/i18n/locales/en/pages.json index 6176dae3..8a0a7ae3 100644 --- a/packages/control-plane-ui/src/i18n/locales/en/pages.json +++ b/packages/control-plane-ui/src/i18n/locales/en/pages.json @@ -79,6 +79,14 @@ "sectionLatencyHealthSubtitle": "Our overhead versus the upstream provider — by the busiest provider in the window.", "latencyOurOverheadP95": "Our Overhead P95", "latencyUpstreamTotalP95": "Upstream P95", + "latencyEndToEndP95": "End-to-End P95", + "source_all": "All", + "source_vk": "Virtual keys", + "source_proxy": "Proxy", + "source_agent": "Agent", + "sourcePickerLabel": "Traffic source", + "complianceWarningTitle": "No compliance hooks are active", + "complianceWarningBody": "No traffic in the selected window was covered by compliance hooks. Enable a hook under Compliance → Hooks & Policies to inspect and govern this traffic.", "latencySlowestProvider": "Slowest Upstream Provider" }, "providers": { diff --git a/packages/control-plane-ui/src/i18n/locales/es/pages.json b/packages/control-plane-ui/src/i18n/locales/es/pages.json index d0e64e7b..19abd57c 100644 --- a/packages/control-plane-ui/src/i18n/locales/es/pages.json +++ b/packages/control-plane-ui/src/i18n/locales/es/pages.json @@ -79,6 +79,14 @@ "sectionLatencyHealthSubtitle": "Nuestro overhead frente al proveedor upstream — según el proveedor con más tráfico en la ventana.", "latencyOurOverheadP95": "Nuestro overhead P95", "latencyUpstreamTotalP95": "Upstream P95", + "latencyEndToEndP95": "Latencia total P95", + "source_all": "Todo", + "source_vk": "Claves virtuales", + "source_proxy": "Proxy", + "source_agent": "Agente", + "sourcePickerLabel": "Fuente de tráfico", + "complianceWarningTitle": "No hay hooks de cumplimiento activos", + "complianceWarningBody": "Ningún tráfico en el período seleccionado fue cubierto por hooks de cumplimiento. Active un hook en Cumplimiento → Hooks y políticas para inspeccionar y gobernar este tráfico.", "latencySlowestProvider": "Proveedor upstream más lento" }, "providers": { diff --git a/packages/control-plane-ui/src/i18n/locales/zh/pages.json b/packages/control-plane-ui/src/i18n/locales/zh/pages.json index 9e165cf2..ed30d093 100644 --- a/packages/control-plane-ui/src/i18n/locales/zh/pages.json +++ b/packages/control-plane-ui/src/i18n/locales/zh/pages.json @@ -79,6 +79,14 @@ "sectionLatencyHealthSubtitle": "对比我方开销与上游提供商耗时 — 取该时间窗口内流量最大的提供商。", "latencyOurOverheadP95": "我方开销 P95", "latencyUpstreamTotalP95": "上游 P95", + "latencyEndToEndP95": "端到端 P95", + "source_all": "全部", + "source_vk": "虚拟密钥", + "source_proxy": "代理", + "source_agent": "代理端", + "sourcePickerLabel": "流量来源", + "complianceWarningTitle": "未启用合规钩子", + "complianceWarningBody": "所选时间窗口内没有流量经过合规钩子处理。请在“合规 → 钩子与策略”中启用钩子,以检查和治理此流量。", "latencySlowestProvider": "最慢的上游提供商" }, "providers": { diff --git a/packages/control-plane-ui/src/pages/dashboard/DashboardPage.module.css b/packages/control-plane-ui/src/pages/dashboard/DashboardPage.module.css index 2f85fa16..67afe5c7 100644 --- a/packages/control-plane-ui/src/pages/dashboard/DashboardPage.module.css +++ b/packages/control-plane-ui/src/pages/dashboard/DashboardPage.module.css @@ -161,6 +161,51 @@ box-shadow: none; } +/* Hero controls row — source picker + time-window picker, wraps on narrow. */ +.heroControls { + display: flex; + gap: var(--g-space-2); + align-items: center; + flex-wrap: wrap; + justify-content: flex-end; +} + +/* Compliance-coverage warning banner — shown when no traffic in the window + * was covered by compliance hooks. */ +.complianceWarning { + display: flex; + align-items: center; + gap: var(--g-space-3); + padding: var(--g-space-3) var(--g-space-4); + border: 1px solid color-mix(in srgb, var(--color-warning) 40%, transparent); + background: color-mix(in srgb, var(--color-warning) 12%, transparent); + border-radius: var(--g-radius-md); +} + +.complianceWarningIcon { + flex-shrink: 0; + font-size: var(--g-font-size-lg); + color: var(--color-warning); + line-height: 1; +} + +.complianceWarningText { + display: flex; + flex-direction: column; + gap: 2px; +} + +.complianceWarningTitle { + font-weight: var(--g-font-weight-semibold); + color: var(--color-text); + font-size: var(--g-font-size-sm); +} + +.complianceWarningBody { + color: var(--color-text-muted); + font-size: var(--g-font-size-xs); +} + /* ── Time window picker (Dashboard PageHeader action) ───────────────────── */ .windowPicker { diff --git a/packages/control-plane-ui/src/pages/dashboard/DashboardPage.tsx b/packages/control-plane-ui/src/pages/dashboard/DashboardPage.tsx index cf2a8cff..59f40345 100644 --- a/packages/control-plane-ui/src/pages/dashboard/DashboardPage.tsx +++ b/packages/control-plane-ui/src/pages/dashboard/DashboardPage.tsx @@ -10,7 +10,7 @@ import { analyticsApi, providerApi } from '@/api/services'; import type { CacheROISummary } from '@/api/services/overview/analytics'; import { proxyApi } from '../../api/services/infrastructure/misc/proxy'; import styles from './DashboardPage.module.css'; -import { WINDOW_MS, type TimeWindow } from './dashboardShared'; +import { WINDOW_MS, type TimeWindow, type TrafficSource, sourceToRollupParam, sourceToLatencyParam } from './dashboardShared'; import { HeroSection } from './HeroSection'; import { HealthCards } from './HealthCards'; import { LatencyHealth } from './LatencyHealth'; @@ -26,6 +26,7 @@ export function DashboardPage() { /* ── Time window state ──────────────────────────────────────────────── */ const [timeWindow, setTimeWindow] = useState('30d'); + const [source, setSource] = useState('all'); const { startTime, endTime } = useMemo(() => { const end = new Date(); @@ -33,15 +34,22 @@ export function DashboardPage() { return { startTime: start.toISOString(), endTime: end.toISOString() }; }, [timeWindow]); + // The source selector maps to two different backend vocabularies: the + // rollup endpoints (summary, by-provider) take vk|proxy|agent; latency-phases + // takes ai-gateway|compliance-proxy|agent. Sparkline/cacheROI have no source + // dimension, so trend charts stay cross-source (noted in the UI copy). + const rollupSourceParam = sourceToRollupParam(source); + const latencySource = sourceToLatencyParam(source); + /* ── Data fetching ──────────────────────────────────────────────────── */ const { data: summary, loading, error, refetch } = useApi( - () => analyticsApi.summary({ startTime, endTime }), - ['admin', 'analytics', 'summary', timeWindow], + () => analyticsApi.summary({ startTime, endTime, ...rollupSourceParam }), + ['admin', 'analytics', 'summary', timeWindow, source], ); const { data: providers } = useApi<{ data: ProviderBreakdown[] }>( - () => analyticsApi.byProvider({ startTime, endTime }), - ['admin', 'analytics', 'by-provider', timeWindow], + () => analyticsApi.byProvider({ startTime, endTime, ...rollupSourceParam }), + ['admin', 'analytics', 'by-provider', timeWindow, source], ); const { data: providerList } = useApi<{ data: Provider[] }>( () => providerApi.list({ ...ADMIN_LIST_FULL_PAGE_PARAMS }), @@ -60,8 +68,8 @@ export function DashboardPage() { // Aggregating by `provider` lets us surface the slowest provider in a // standalone callout without a second round-trip. const { data: latencyPhases } = useApi( - () => analyticsApi.latencyPhases({ groupBy: 'provider', start: startTime, end: endTime }), - ['admin', 'analytics', 'latency-phases', 'dashboard', timeWindow], + () => analyticsApi.latencyPhases({ groupBy: 'provider', start: startTime, end: endTime, source: latencySource }), + ['admin', 'analytics', 'latency-phases', 'dashboard', timeWindow, source], ); /* ── Compliance Proxy ───────────────────────────────────────────────── */ @@ -173,6 +181,11 @@ export function DashboardPage() { const windowLabel = t(`pages:dashboard.win${timeWindow}` as never); + // Surface a warning when the proxy is reachable but nothing in the window + // was covered by compliance hooks — a 0% figure otherwise reads as a metric, + // not the governance gap it actually is. + const showComplianceWarning = proxyReachable === true && proxyTotalRequests > 0 && proxyCoveragePercent === 0; + /* ── Render ─────────────────────────────────────────────────────────── */ return ( @@ -180,6 +193,8 @@ export function DashboardPage() { + {showComplianceWarning && ( +
+ + + {t('pages:dashboard.complianceWarningTitle')} + {t('pages:dashboard.complianceWarningBody')} + +
+ )} + void; + source: TrafficSource; + setSource: (s: TrafficSource) => void; animRequests: number; vkRequests: number; proxyTotalRequests: number; @@ -21,6 +23,8 @@ interface HeroSectionProps { export function HeroSection({ timeWindow, setTimeWindow, + source, + setSource, animRequests, vkRequests, proxyTotalRequests, @@ -46,17 +50,31 @@ export function HeroSection({

{t('pages:dashboard.title')}

{t('pages:dashboard.heroSubtitle')}

-
- {WINDOW_OPTIONS.map((w) => ( - - ))} +
+
+ {SOURCE_OPTIONS.map((s) => ( + + ))} +
+
+ {WINDOW_OPTIONS.map((w) => ( + + ))} +
diff --git a/packages/control-plane-ui/src/pages/dashboard/LatencyHealth.tsx b/packages/control-plane-ui/src/pages/dashboard/LatencyHealth.tsx index a8928f47..b0843bfe 100644 --- a/packages/control-plane-ui/src/pages/dashboard/LatencyHealth.tsx +++ b/packages/control-plane-ui/src/pages/dashboard/LatencyHealth.tsx @@ -30,7 +30,7 @@ export function LatencyHealth({ latencyPhases, navigate }: LatencyHealthProps) { {t('pages:dashboard.sectionLatencyHealthSubtitle', 'Our overhead versus the upstream provider — by the busiest provider in the window.')}

- +
{t('pages:dashboard.latencyOurOverheadP95', 'Our Overhead P95')}
@@ -45,6 +45,13 @@ export function LatencyHealth({ latencyPhases, navigate }: LatencyHealthProps) {
{top.groupLabel}
+ +
{t('pages:dashboard.latencyEndToEndP95', 'End-to-End P95')}
+
+ {fmtMs(top.totalP95Ms)} +
+ {top.groupLabel} +
navigate('/analytics')}>
{t('pages:dashboard.latencySlowestProvider', 'Slowest Upstream Provider')}
diff --git a/packages/control-plane-ui/src/pages/dashboard/dashboardShared.tsx b/packages/control-plane-ui/src/pages/dashboard/dashboardShared.tsx index 8186f079..eeb915da 100644 --- a/packages/control-plane-ui/src/pages/dashboard/dashboardShared.tsx +++ b/packages/control-plane-ui/src/pages/dashboard/dashboardShared.tsx @@ -14,6 +14,33 @@ export const WINDOW_MS: Record = { export const WINDOW_OPTIONS: TimeWindow[] = ['1h', '1d', '7d', '30d']; +/* ── Traffic source ─────────────────────────────────────────────────────── */ + +// The live traffic sources the analytics endpoints can filter by. NOT a +// benchmark/environment dimension — synthetic benchmark runs live on the +// separate Benchmarks surface, and there is no environment column in the +// analytics rollups, so neither is offered here (would be a dead control). +export type TrafficSource = 'all' | 'vk' | 'proxy' | 'agent'; + +export const SOURCE_OPTIONS: TrafficSource[] = ['all', 'vk', 'proxy', 'agent']; + +// Rollup endpoints (summary, by-provider) accept ?source=vk|proxy|agent +// (empty = all) per sourceSubDimension() in analytics_rollup.go. +export function sourceToRollupParam(s: TrafficSource): Record { + return s === 'all' ? {} : { source: s }; +} + +// latency-phases accepts ?source=ai-gateway|compliance-proxy|agent|all per +// analytics_latency.go — a different vocabulary from the rollup endpoints. +export function sourceToLatencyParam(s: TrafficSource): 'all' | 'ai-gateway' | 'compliance-proxy' | 'agent' { + switch (s) { + case 'vk': return 'ai-gateway'; + case 'proxy': return 'compliance-proxy'; + case 'agent': return 'agent'; + default: return 'all'; + } +} + /* ── Info icon ──────────────────────────────────────────────────────────── */ export function InfoIcon({ description }: { description: string }) {