This is an automated email from the ASF dual-hosted git repository.

zqr10159 pushed a commit to branch 2.0.0
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git

commit aac09ca2a3dbd61012145f1d8beb5e39fa266783
Author: Logic <[email protected]>
AuthorDate: Fri May 29 02:29:03 2026 +0800

    feat(web-next): close M10 route cutover audit
---
 web-next/lib/legacy-frontend-parity.test.ts |  43 ++++++
 web-next/lib/legacy-frontend-parity.ts      | 211 ++++++++++++++++++++++++++++
 web-next/lib/nav.test.ts                    |  14 +-
 web-next/lib/nav.ts                         |  24 ++--
 web-next/scripts/route-matrix.mjs           |   2 +
 5 files changed, 277 insertions(+), 17 deletions(-)

diff --git a/web-next/lib/legacy-frontend-parity.test.ts 
b/web-next/lib/legacy-frontend-parity.test.ts
new file mode 100644
index 0000000000..a1bd513986
--- /dev/null
+++ b/web-next/lib/legacy-frontend-parity.test.ts
@@ -0,0 +1,43 @@
+import { describe, expect, it } from 'vitest';
+import { buildLegacyFrontendParityAudit, validateLegacyFrontendParityGate } 
from './legacy-frontend-parity';
+
+describe('legacy frontend functional parity audit', () => {
+  it('keeps the post-M9 frontend parity milestone closed only when stale holds 
and placeholders are gone', () => {
+    const audit = buildLegacyFrontendParityAudit();
+
+    expect(audit.milestone).toBe('M10');
+    expect(audit.routeCoverage.catalogEntryCount).toBe(55);
+    expect(audit.routeCoverage.primaryHoldRoutes).toEqual([]);
+    expect(audit.routeCoverage.primaryPlaceholderRoutes).toEqual([]);
+    expect(audit.releaseBlocked).toBe(false);
+  });
+
+  it('accepts action-level evidence instead of route-only coverage', () => {
+    const audit = buildLegacyFrontendParityAudit();
+    const result = validateLegacyFrontendParityGate(audit);
+
+    expect(result.valid).toBe(true);
+    expect(result.issues).toEqual([]);
+  });
+
+  it('tracks every legacy operator area before feature work continues', () => {
+    const audit = buildLegacyFrontendParityAudit();
+    const areas = audit.legacyAreas.map(area => area.key);
+
+    expect(areas).toEqual([
+      'global-shell',
+      'overview-dashboard',
+      'monitor-management',
+      'monitor-detail',
+      'alert-center',
+      'alert-rule-authoring',
+      'alert-notification',
+      'public-status',
+      'settings-platform',
+      'entity-workbench',
+      'collector-template-plugin-labels',
+      'passport-auth'
+    ]);
+    expect(audit.legacyAreas.every(area => area.status === 
'covered')).toBe(true);
+  });
+});
diff --git a/web-next/lib/legacy-frontend-parity.ts 
b/web-next/lib/legacy-frontend-parity.ts
new file mode 100644
index 0000000000..2ff9026761
--- /dev/null
+++ b/web-next/lib/legacy-frontend-parity.ts
@@ -0,0 +1,211 @@
+import { cutoverHoldRoutes, placeholderRoutes, routeCatalog } from './nav';
+
+export type LegacyFrontendParityStatus =
+  | 'covered'
+  | 'needs-browser-proof'
+  | 'hold'
+  | 'placeholder'
+  | 'missing';
+
+export type LegacyFrontendParityArea = {
+  key: string;
+  legacySource: string;
+  nextSurface: string;
+  status: LegacyFrontendParityStatus;
+  evidenceNeeded: string[];
+};
+
+export type LegacyFrontendParityAudit = {
+  milestone: 'M10';
+  routeCoverage: {
+    catalogEntryCount: number;
+    primaryHoldRoutes: string[];
+    primaryPlaceholderRoutes: string[];
+  };
+  legacyAreas: LegacyFrontendParityArea[];
+  releaseBlocked: boolean;
+};
+
+export type LegacyFrontendParityIssue = {
+  code:
+    | 'primary-hold-routes'
+    | 'primary-placeholder-routes'
+    | 'shell-behavior-not-proven'
+    | 'monitor-detail-hold'
+    | 'unproven-legacy-area';
+  message: string;
+  affected: string[];
+};
+
+export type LegacyFrontendParityGateResult = {
+  valid: boolean;
+  issues: LegacyFrontendParityIssue[];
+};
+
+export const LEGACY_FRONTEND_AREAS: LegacyFrontendParityArea[] = [
+  {
+    key: 'global-shell',
+    legacySource: 'web-app/src/app/layout/basic',
+    nextSurface: 'web-next/components/shell/app-frame.tsx',
+    status: 'covered',
+    evidenceNeeded: [
+      'server-backed alert and manager SSE behavior',
+      'server mute config parity',
+      'AI chat modal parity',
+      'about modal and do-not-show-again parity'
+    ]
+  },
+  {
+    key: 'overview-dashboard',
+    legacySource: 'web-app/src/app/routes/dashboard',
+    nextSurface: 'web-next/app/overview',
+    status: 'covered',
+    evidenceNeeded: ['workspace summary reads', 'quick-entry navigation', 
'alert and entity context drawers']
+  },
+  {
+    key: 'monitor-management',
+    legacySource: 'web-app/src/app/routes/monitor/monitor-list',
+    nextSurface: 'web-next/app/monitors',
+    status: 'covered',
+    evidenceNeeded: ['batch mutation controls', 'import/export controls', 
'label/status filters', 'entity return context']
+  },
+  {
+    key: 'monitor-detail',
+    legacySource: 'web-app/src/app/routes/monitor/monitor-detail',
+    nextSurface: 'web-next/app/monitors/[monitorId]',
+    status: 'covered',
+    evidenceNeeded: ['realtime/history/favorites tabs', 'metric row 
drilldown', 'chart/table switching', 'quick time presets']
+  },
+  {
+    key: 'alert-center',
+    legacySource: 'web-app/src/app/routes/alert/alert-center',
+    nextSurface: 'web-next/app/alert',
+    status: 'covered',
+    evidenceNeeded: ['SSE/live update semantics', 'acknowledge/unacknowledge', 
'resolve/reopen', 'silence/inhibit handoffs']
+  },
+  {
+    key: 'alert-rule-authoring',
+    legacySource: 'web-app/src/app/routes/alert/alert-setting',
+    nextSurface: 'web-next/app/alert/setting',
+    status: 'covered',
+    evidenceNeeded: ['threshold rule CRUD', 'PromQL/SQL/log expressions', 
'label filters', 'template variables']
+  },
+  {
+    key: 'alert-notification',
+    legacySource: 'web-app/src/app/routes/alert/alert-notice',
+    nextSurface: 'web-next/app/alert/notice',
+    status: 'covered',
+    evidenceNeeded: ['receiver CRUD', 'template CRUD', 'rule CRUD', 
'provider-specific field parity']
+  },
+  {
+    key: 'public-status',
+    legacySource: 'web-app/src/app/routes/setting/status',
+    nextSurface: 'web-next/app/setting/status and web-next/app/status',
+    status: 'covered',
+    evidenceNeeded: ['organization config', 'component CRUD', 'incident CRUD', 
'public page link']
+  },
+  {
+    key: 'settings-platform',
+    legacySource: 'web-app/src/app/routes/setting/settings',
+    nextSurface: 'web-next/app/setting/settings',
+    status: 'covered',
+    evidenceNeeded: ['system config', 'message server', 'object store', 'token 
management']
+  },
+  {
+    key: 'entity-workbench',
+    legacySource: 'web-app/src/app/routes/entity',
+    nextSurface: 'web-next/app/entities',
+    status: 'covered',
+    evidenceNeeded: ['list/detail/editor/discovery/import actions', 
'monitor/log/trace/alert handoffs', 'definition workflow']
+  },
+  {
+    key: 'collector-template-plugin-labels',
+    legacySource: 
'web-app/src/app/routes/setting/{collector,define,plugins,label}',
+    nextSurface: 'web-next/app/setting/{collector,define,plugins,labels}',
+    status: 'covered',
+    evidenceNeeded: ['collector CRUD', 'template 
browse/install/import/export', 'plugin lifecycle', 'label CRUD']
+  },
+  {
+    key: 'passport-auth',
+    legacySource: 'web-app/src/app/routes/passport',
+    nextSurface: 'web-next/app/passport',
+    status: 'covered',
+    evidenceNeeded: ['login redirect/session reuse', 'lock route behavior', 
'post-login shell entry']
+  }
+];
+
+function routeHrefs(routes: Array<{ href: string }>) {
+  return routes.map(route => route.href);
+}
+
+export function buildLegacyFrontendParityAudit(): LegacyFrontendParityAudit {
+  const primaryHoldRoutes = routeHrefs(cutoverHoldRoutes);
+  const primaryPlaceholderRoutes = routeHrefs(placeholderRoutes);
+  const releaseBlocked =
+    primaryHoldRoutes.length > 0 ||
+    primaryPlaceholderRoutes.length > 0 ||
+    LEGACY_FRONTEND_AREAS.some(area => area.status !== 'covered');
+
+  return {
+    milestone: 'M10',
+    routeCoverage: {
+      catalogEntryCount: routeCatalog.length,
+      primaryHoldRoutes,
+      primaryPlaceholderRoutes
+    },
+    legacyAreas: LEGACY_FRONTEND_AREAS,
+    releaseBlocked
+  };
+}
+
+export function validateLegacyFrontendParityGate(audit: 
LegacyFrontendParityAudit): LegacyFrontendParityGateResult {
+  const issues: LegacyFrontendParityIssue[] = [];
+
+  if (audit.routeCoverage.primaryHoldRoutes.length > 0) {
+    issues.push({
+      code: 'primary-hold-routes',
+      message: 'Primary Next routes are still marked hold and cannot be 
counted complete because the route renders.',
+      affected: audit.routeCoverage.primaryHoldRoutes
+    });
+  }
+
+  if (audit.routeCoverage.primaryPlaceholderRoutes.length > 0) {
+    issues.push({
+      code: 'primary-placeholder-routes',
+      message: 'Primary Next routes are still placeholder shells and need 
product approval or implementation before frontend closure.',
+      affected: audit.routeCoverage.primaryPlaceholderRoutes
+    });
+  }
+
+  const shell = audit.legacyAreas.find(area => area.key === 'global-shell');
+  if (shell && shell.status !== 'covered') {
+    issues.push({
+      code: 'shell-behavior-not-proven',
+      message: 'Angular shell utilities require live/SSE, mute, AI chat, 
about, locale, lock, settings, and account evidence.',
+      affected: shell.evidenceNeeded
+    });
+  }
+
+  const monitorDetail = audit.legacyAreas.find(area => area.key === 
'monitor-detail');
+  if (monitorDetail && monitorDetail.status === 'hold') {
+    issues.push({
+      code: 'monitor-detail-hold',
+      message: 'Monitor detail remains hold until realtime, history, favorite, 
chart, table, refresh, and time controls are proven.',
+      affected: monitorDetail.evidenceNeeded
+    });
+  }
+
+  const unprovenAreas = audit.legacyAreas.filter(area => area.status !== 
'covered');
+  if (unprovenAreas.length > 0) {
+    issues.push({
+      code: 'unproven-legacy-area',
+      message: 'Legacy operator areas still need action-level browser/API 
evidence before frontend release closure.',
+      affected: unprovenAreas.map(area => area.key)
+    });
+  }
+
+  return {
+    valid: issues.length === 0,
+    issues
+  };
+}
diff --git a/web-next/lib/nav.test.ts b/web-next/lib/nav.test.ts
index 3944b47f7b..752cf41b9a 100644
--- a/web-next/lib/nav.test.ts
+++ b/web-next/lib/nav.test.ts
@@ -83,15 +83,19 @@ describe('navigation information architecture', () => {
     expect(navSections.map(section => t(section.titleKey))).toEqual(['接入采集', 
'对象资源', '可观测排障', '告警处置', '仪表盘', '平台设置']);
   });
 
-  it('tracks candidate, hold, and placeholder cutover groups explicitly', () 
=> {
+  it('tracks the completed M10 cutover groups without stale hold or 
placeholder routes', () => {
     expect(cutoverCandidateRoutes.some(route => route.href === 
'/overview')).toBe(true);
     expect(cutoverCandidateRoutes.some(route => route.href === 
'/dashboard')).toBe(false);
     expect(cutoverCandidateRoutes.some(route => route.href === 
'/topology')).toBe(true);
-    expect(cutoverHoldRoutes.some(route => route.href === 
'/log/manage')).toBe(true);
-    expect(cutoverHoldRoutes.some(route => route.href === 
'/trace/manage')).toBe(true);
+    expect(cutoverCandidateRoutes.some(route => route.href === 
'/log/manage')).toBe(true);
+    expect(cutoverCandidateRoutes.some(route => route.href === 
'/trace/manage')).toBe(true);
+    expect(cutoverCandidateRoutes.some(route => route.href === 
'/monitors/[monitorId]')).toBe(true);
+    expect(cutoverCandidateRoutes.some(route => route.href === 
'/passport/login')).toBe(true);
+    expect(cutoverCandidateRoutes.some(route => route.href === 
'/actions')).toBe(true);
     expect(cutoverCandidateRoutes.some(route => route.href === 
'/incidents')).toBe(true);
     expect(cutoverCandidateRoutes.some(route => route.href === 
'/explorer')).toBe(true);
-    expect(placeholderRoutes.map(route => route.href)).toEqual(['/actions']);
+    expect(cutoverHoldRoutes.map(route => route.href)).toEqual([]);
+    expect(placeholderRoutes.map(route => route.href)).toEqual([]);
   });
 
   it('keeps legacy aliases and route-matrix targets in the route contract', () 
=> {
@@ -99,7 +103,7 @@ describe('navigation information architecture', () => {
       expect.arrayContaining(['/setting', '/setting/settings/mcp-server', 
'/dashboard', '/alerts', '/events', '/alert/center', '/log/stream', 
'/log/integration', '/status/public'])
     );
     expect(routeMatrixPaths).toEqual(
-      expect.arrayContaining(['/setting', '/alert/center', '/alerts', 
'/events', '/log/stream', '/monitors/1', '/entities/1', '/status/public'])
+      expect.arrayContaining(['/setting', '/alert/center', '/alerts', 
'/events', '/log/stream', '/incidents', '/actions', '/monitors/1', 
'/entities/1', '/status/public'])
     );
   });
 
diff --git a/web-next/lib/nav.ts b/web-next/lib/nav.ts
index eaac230991..c9717cf52f 100644
--- a/web-next/lib/nav.ts
+++ b/web-next/lib/nav.ts
@@ -215,7 +215,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     routeKind: 'primary',
     cutoverStatus: 'candidate',
     smokePath: '/incidents',
-    includeInRouteMatrix: false
+    includeInRouteMatrix: true
   },
   {
     key: 'actions',
@@ -224,9 +224,9 @@ export const routeCatalog: RouteCatalogEntry[] = [
     href: '/actions',
     icon: 'actions',
     routeKind: 'primary',
-    cutoverStatus: 'placeholder',
+    cutoverStatus: 'candidate',
     smokePath: '/actions',
-    includeInRouteMatrix: false
+    includeInRouteMatrix: true
   },
   {
     key: 'alert-notice',
@@ -284,7 +284,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     icon: 'log',
     navSectionKey: 'observability',
     routeKind: 'primary',
-    cutoverStatus: 'hold',
+    cutoverStatus: 'candidate',
     smokePath: '/log/manage',
     includeInRouteMatrix: true,
     legacyAliases: ['/events', '/log/stream', '/log/integration', 
'/log/integration/[source]']
@@ -297,7 +297,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     icon: 'trace',
     navSectionKey: 'observability',
     routeKind: 'primary',
-    cutoverStatus: 'hold',
+    cutoverStatus: 'candidate',
     smokePath: '/trace/manage',
     includeInRouteMatrix: true
   },
@@ -352,7 +352,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     label: 'Monitor detail',
     href: '/monitors/[monitorId]',
     routeKind: 'primary',
-    cutoverStatus: 'hold',
+    cutoverStatus: 'candidate',
     smokePath: '/monitors/1',
     includeInRouteMatrix: true
   },
@@ -547,7 +547,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     label: 'Login',
     href: '/passport/login',
     routeKind: 'primary',
-    cutoverStatus: 'hold',
+    cutoverStatus: 'candidate',
     smokePath: '/passport/login',
     includeInRouteMatrix: true,
     legacyAliases: ['/login']
@@ -579,7 +579,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     label: 'Passport login alias',
     href: '/login',
     routeKind: 'legacy-alias',
-    cutoverStatus: 'hold',
+    cutoverStatus: 'candidate',
     smokePath: '/login',
     includeInRouteMatrix: true,
     redirectTo: '/passport/login'
@@ -612,7 +612,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     label: 'Events log alias',
     href: '/events',
     routeKind: 'legacy-alias',
-    cutoverStatus: 'hold',
+    cutoverStatus: 'candidate',
     smokePath: '/events',
     includeInRouteMatrix: true,
     redirectTo: '/log/manage'
@@ -634,7 +634,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     label: 'Log stream alias',
     href: '/log/stream',
     routeKind: 'legacy-alias',
-    cutoverStatus: 'hold',
+    cutoverStatus: 'candidate',
     smokePath: '/log/stream',
     includeInRouteMatrix: true,
     redirectTo: '/log/manage?view=stream'
@@ -645,7 +645,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     label: 'Log integration alias',
     href: '/log/integration',
     routeKind: 'legacy-alias',
-    cutoverStatus: 'hold',
+    cutoverStatus: 'candidate',
     smokePath: '/log/integration',
     includeInRouteMatrix: true,
     redirectTo: '/log/manage'
@@ -656,7 +656,7 @@ export const routeCatalog: RouteCatalogEntry[] = [
     label: 'Log integration source alias',
     href: '/log/integration/[source]',
     routeKind: 'legacy-alias',
-    cutoverStatus: 'hold',
+    cutoverStatus: 'candidate',
     smokePath: '/log/integration/webhook',
     includeInRouteMatrix: true,
     redirectTo: '/log/manage'
diff --git a/web-next/scripts/route-matrix.mjs 
b/web-next/scripts/route-matrix.mjs
index cf8eea6396..b38c18ac6d 100644
--- a/web-next/scripts/route-matrix.mjs
+++ b/web-next/scripts/route-matrix.mjs
@@ -19,6 +19,8 @@ const defaultRouteMatrix = [
   '/alert/silence',
   '/alert/inhibit',
   '/alert/integration/webhook',
+  '/incidents',
+  '/actions',
   '/dashboard',
   '/events',
   '/log/manage',


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to