diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index 42331ce4..07013f1b 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -8,6 +8,7 @@ import { Dashboard } from './pages/Dashboard'; import { History } from './pages/History'; import { Mileage } from './pages/Mileage'; import { NotificationRules } from './pages/NotificationRules'; +import { OpsQuality } from './pages/OpsQuality'; import { Quality } from './pages/Quality'; import { Realtime } from './pages/Realtime'; import { VehicleDetail } from './pages/VehicleDetail'; @@ -154,7 +155,7 @@ export default function App() { replaceQualityHash(qualityFilters); return; } - if (page === 'notification-rules') { + if (page === 'notification-rules' || page === 'ops-quality') { replaceHash(page); return; } @@ -415,7 +416,8 @@ export default function App() { setLinkIssueCount(health.linkHealth.filter((item) => item.status !== 'ok').length); setPlatformRelease(health.runtime?.platformRelease ?? ''); }} onFiltersChange={updateQualityFilters} initialFilters={qualityFilters} />, - 'notification-rules': + 'notification-rules': , + 'ops-quality': }; return ( diff --git a/vehicle-data-platform/apps/web/src/domain/appRoute.test.ts b/vehicle-data-platform/apps/web/src/domain/appRoute.test.ts index 145680e9..891b18e7 100644 --- a/vehicle-data-platform/apps/web/src/domain/appRoute.test.ts +++ b/vehicle-data-platform/apps/web/src/domain/appRoute.test.ts @@ -55,6 +55,13 @@ describe('parseAppHash', () => { }); }); + test('parses ops quality page', () => { + expect(parseAppHash('#/ops-quality')).toEqual({ + page: 'ops-quality', + filters: {} + }); + }); + test('ignores unknown pages', () => { expect(parseAppHash('#/unknown?keyword=VIN001')).toEqual({}); }); @@ -81,6 +88,10 @@ describe('buildAppHash', () => { expect(buildAppHash({ page: 'notification-rules' })).toBe('#/notification-rules'); }); + test('builds ops quality page hash', () => { + expect(buildAppHash({ page: 'ops-quality' })).toBe('#/ops-quality'); + }); + test('builds page-only hash when keyword is empty', () => { expect(buildAppHash({ page: 'quality', keyword: '' })).toBe('#/quality'); }); diff --git a/vehicle-data-platform/apps/web/src/domain/appRoute.ts b/vehicle-data-platform/apps/web/src/domain/appRoute.ts index e4b00285..3181c0fe 100644 --- a/vehicle-data-platform/apps/web/src/domain/appRoute.ts +++ b/vehicle-data-platform/apps/web/src/domain/appRoute.ts @@ -1,6 +1,6 @@ import type { PageKey } from '../layout/AppShell'; -const pageKeys = new Set(['dashboard', 'vehicles', 'realtime', 'detail', 'history', 'mileage', 'quality', 'notification-rules']); +const pageKeys = new Set(['dashboard', 'vehicles', 'realtime', 'detail', 'history', 'mileage', 'quality', 'notification-rules', 'ops-quality']); export type AppRoute = { page?: PageKey; diff --git a/vehicle-data-platform/apps/web/src/layout/AppShell.tsx b/vehicle-data-platform/apps/web/src/layout/AppShell.tsx index 72a1ab89..ab3c286f 100644 --- a/vehicle-data-platform/apps/web/src/layout/AppShell.tsx +++ b/vehicle-data-platform/apps/web/src/layout/AppShell.tsx @@ -8,14 +8,15 @@ import { IconSearch, IconServer, IconSetting, - IconBell + IconBell, + IconPulse } from '@douyinfe/semi-icons'; import type { ReactNode } from 'react'; import { useState } from 'react'; import type { VehicleSourceConsistency, VehicleServiceStatus } from '../api/types'; import { isAMapConfigured } from '../config/appConfig'; -export type PageKey = 'dashboard' | 'vehicles' | 'realtime' | 'detail' | 'history' | 'mileage' | 'quality' | 'notification-rules'; +export type PageKey = 'dashboard' | 'vehicles' | 'realtime' | 'detail' | 'history' | 'mileage' | 'quality' | 'notification-rules' | 'ops-quality'; const navItems = [ { itemKey: 'dashboard', text: '运营驾驶舱', icon: }, @@ -25,7 +26,8 @@ const navItems = [ { itemKey: 'history', text: '轨迹回放', icon: }, { itemKey: 'mileage', text: '统计分析', icon: }, { itemKey: 'quality', text: '告警事件', icon: }, - { itemKey: 'notification-rules', text: '通知规则', icon: } + { itemKey: 'notification-rules', text: '通知规则', icon: }, + { itemKey: 'ops-quality', text: '运维质量', icon: } ]; function linkHealthClassName(count: number | null) { @@ -119,6 +121,7 @@ export function AppShell({ + } + /> + +
+ +
{formatNumber(health?.kafkaLag)}
+
Kafka Lag
+
+ +
{formatNumber(health?.activeConnections)}
+
活跃连接
+
+ +
{formatNumber(health?.redisOnlineKeys)}
+
Redis 在线 Key
+
+ +
{runtime?.platformRelease || '-'}
+
运行版本
+
+
+ + +
+
+ TDengine 写入 +
{statusText(health?.tdengineWritable)}
+
时序历史、RAW 和统计依赖 TDengine 可写状态。
+
+
+ MySQL 写入 +
{statusText(health?.mysqlWritable)}
+
车辆身份、绑定、服务配置和中台查询依赖 MySQL。
+
+
+ 请求超时 +
{runtime?.requestTimeoutMs ? `${runtime.requestTimeoutMs.toLocaleString()} ms` : '-'}
+
接口层读取 Redis、TDengine、MySQL 的统一超时保护。
+
+
+
+ + + + {runtime?.amapWebJsConfigured ? 'Web JS Key 已配置' : 'Web JS Key 未配置'} + {amapSecurityText(health)} + {runtime?.amapSecurityServiceHost || '代理未启用'} + + + + {health?.capacityFindings?.length ? ( + + + {health.capacityFindings.map((item) => ( + {item} + ))} + + + ) : null} + + + + pagination={false} + dataSource={health?.linkHealth ?? []} + rowKey={(row?: LinkHealth) => `${row?.name ?? ''}-${row?.status ?? ''}`} + columns={[ + { title: '链路', dataIndex: 'name' }, + { title: '状态', width: 120, render: (_: unknown, row: LinkHealth) => {row.status} }, + { title: '说明', dataIndex: 'detail' } + ]} + /> + + + ); +} diff --git a/vehicle-data-platform/apps/web/src/test/App.test.tsx b/vehicle-data-platform/apps/web/src/test/App.test.tsx index 3ceee7b9..4d1dd879 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -3259,6 +3259,65 @@ test('renders notification rules as a standalone operations page', async () => { expect(writeText).toHaveBeenCalledWith(expect.stringContaining('无来源规则 / P0 / 平台接入')); }); +test('renders ops quality as a standalone runtime health page', async () => { + window.history.replaceState(null, '', '/#/ops-quality'); + const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + if (path.includes('/api/ops/health')) { + return { + ok: true, + json: async () => ({ + data: { + linkHealth: [ + { name: 'gb32960-gateway', status: 'ok', detail: 'listening :32960' }, + { name: 'kafka', status: 'warning', detail: 'lag 42' } + ], + kafkaLag: 42, + activeConnections: 1234, + capacityFindings: ['Kafka lag 42', 'Redis online key below expected'], + redisOnlineKeys: 368, + tdengineWritable: true, + mysqlWritable: false, + runtime: { + requestTimeoutMs: 5000, + platformRelease: 'platform-ops-test', + amapWebJsConfigured: true, + amapSecurityProxyEnabled: true, + amapSecurityCodeExposed: false, + amapSecurityServiceHost: '/_AMapService' + } + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + return { + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 20, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + expect(await screen.findByRole('heading', { name: '运维质量' })).toBeInTheDocument(); + expect(screen.getByText('platform-ops-test')).toBeInTheDocument(); + expect(screen.getByText('42')).toBeInTheDocument(); + expect(screen.getByText('1,234')).toBeInTheDocument(); + expect(screen.getByText('368')).toBeInTheDocument(); + expect(screen.getByText('TDengine 写入')).toBeInTheDocument(); + expect(screen.getByText('MySQL 写入')).toBeInTheDocument(); + expect(screen.getByText('/_AMapService')).toBeInTheDocument(); + expect(screen.getByText('安全码未暴露')).toBeInTheDocument(); + expect(screen.getByText('gb32960-gateway')).toBeInTheDocument(); + expect(screen.getByText('Kafka lag 42')).toBeInTheDocument(); + expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/ops/health'), undefined); +}); + test('copies notification text from quality priority queue', async () => { window.history.replaceState(null, '', '/#/quality'); const writeText = vi.fn(() => Promise.resolve()); diff --git a/vehicle-data-platform/docs/vehicle-platform-blueprint.md b/vehicle-data-platform/docs/vehicle-platform-blueprint.md index 43304a6e..6f5303ca 100644 --- a/vehicle-data-platform/docs/vehicle-platform-blueprint.md +++ b/vehicle-data-platform/docs/vehicle-platform-blueprint.md @@ -41,8 +41,8 @@ The left navigation should be vehicle-service oriented: 6. History Query 7. Alert Events 8. Statistics -9. Ops Quality -10. Notification Rules +9. Notification Rules +10. Ops Quality The global search stays persistent in the top bar. It accepts VIN, plate, or JT808 phone and resolves to one vehicle context. The selected vehicle context should travel across all pages. @@ -265,6 +265,7 @@ Protocol-specific data appears as source evidence fields in these responses. New - Expand `VehicleMap` for selected marker, fit controls, polyline playback, and large-point fallback. - Add replay controls on History. - Add AMap readiness diagnostics to Ops Quality and Dashboard. +- Keep Ops Quality as a standalone runtime page for gateway, queue, Redis, TDengine, MySQL, AMap, and release checks. ### Phase 3: Alert Events