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]
