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 c4d242fa6af1dd28a8d9527c82fef1574d55d4c0
Author: Logic <[email protected]>
AuthorDate: Fri May 29 02:35:16 2026 +0800

    feat(web-next): add M10 frontend convergence gate
---
 web-app/src/assets/i18n/en-US.json                 |  54 ++++++++-
 web-app/src/assets/i18n/zh-CN.json                 |  56 ++++++++-
 web-next/lib/types.ts                              |  30 +++++
 .../scripts/frontend-convergence-contract.test.ts  | 133 +++++++++++++++++++++
 4 files changed, 266 insertions(+), 7 deletions(-)

diff --git a/web-app/src/assets/i18n/en-US.json 
b/web-app/src/assets/i18n/en-US.json
index 7a0eb3fff4..42add5ad74 100644
--- a/web-app/src/assets/i18n/en-US.json
+++ b/web-app/src/assets/i18n/en-US.json
@@ -1427,6 +1427,43 @@
   "common.week.5": "Friday",
   "common.week.6": "Saturday",
   "common.week.7": "Sunday",
+  "time.range.preset": "Time range",
+  "time.range.relative": "Relative",
+  "time.range.start": "Start",
+  "time.range.end": "End",
+  "time.range.from": "From",
+  "time.range.to": "To",
+  "time.range.absolute-title": "Absolute time range",
+  "time.range.quick-ranges": "Quick ranges",
+  "time.range.recent-ranges": "Recent ranges",
+  "time.range.custom-range": "Custom range",
+  "time.range.custom-name": "Range name",
+  "time.range.save-custom-range": "Save range",
+  "time.range.delete-custom-range": "Delete",
+  "time.range.validation-valid": "Valid time expression",
+  "time.range.validation-invalid": "Invalid time expression",
+  "time.range.year": "Year",
+  "time.range.month": "Month",
+  "time.range.date": "Date",
+  "time.range.unset": "Not set",
+  "time.range.hour": "Hour",
+  "time.range.minute": "Minute",
+  "time.range.second": "Second",
+  "time.range.previous-month": "Previous month",
+  "time.range.next-month": "Next month",
+  "time.range.refresh": "Refresh interval",
+  "time.range.manual-refresh": "Manual",
+  "time.range.live": "Live",
+  "time.range.live-on": "Pause live updates",
+  "time.range.live-off": "Resume live updates",
+  "time.range.timezone": "Timezone",
+  "time.range.local-timezone": "Local timezone",
+  "time.range.apply": "Apply",
+  "time.range.apply-aria": "Apply time range",
+  "time.range.refresh-action": "Refresh now",
+  "time.range.reset": "Reset",
+  "time.range.reset-aria": "Reset time range",
+  "time.range.relative-placeholder": "45m",
   "common.yes": "Yes",
   "dashboard.alerts.deal": "Alarms Dealing",
   "dashboard.alerts.deal-percent": "Dealing Rate",
@@ -3022,7 +3059,7 @@
   "monitors.loading": "Loading the monitor list from the existing monitor 
API.",
   "monitors.kicker": "Monitoring Workspace",
   "monitors.title": "Monitoring Center",
-  "monitors.subtitle": "Monitoring filters, result list, and detail rail now 
share the same workspace shell.",
+  "monitors.subtitle": "Monitor collector-backed resources, status, labels, 
and lifecycle actions.",
   "monitors.workspace": "Monitoring",
   "monitors.fact.total": "Total monitors",
   "monitors.fact.page-count": "Current page",
@@ -3037,9 +3074,15 @@
   "monitors.labels.placeholder": "key=value or keyword",
   "common.status": "Status",
   "monitors.status.all": "All statuses",
-  "monitors.status.up": "Running",
-  "monitors.status.down": "Abnormal",
+  "monitors.status.up": "Up",
+  "monitors.status.down": "Down",
   "monitors.status.paused": "Paused",
+  "monitors.type.all": "All types",
+  "monitors.app-picker.catalog-title": "Type catalog",
+  "monitors.app-picker.close": "Close",
+  "monitors.app-picker.empty": "No matching types",
+  "monitors.app-picker.item-count": "items",
+  "monitors.app-picker.search-placeholder": "Search visible names",
   "monitors.section.list.title": "Monitor List",
   "monitors.section.list.copy": "Results are consolidated into a flat 
workspace list.",
   "monitors.empty-results.title": "No Matching Monitors",
@@ -3222,6 +3265,8 @@
   "common.none": "None",
   "common.clear-selection": "Clear selection",
   "common.select-all": "Select page",
+  "common.select-page": "Select page",
+  "common.select-all-results": "Select all",
   "common.import": "Import monitors",
   "common.selected": "Selected",
   "common.export-selected-json": "Export selected JSON",
@@ -3233,12 +3278,15 @@
   "common.page-size": "Page size",
   "common.previous": "Previous",
   "common.next": "Next",
+  "common.previous-page": "Previous page",
+  "common.next-page": "Next page",
   "common.pause": "Pause",
   "common.delete-success": "Deleted successfully",
   "common.delete-failed": "Delete failed",
   "monitor.copy.action": "Copy monitor",
   "monitor.detail.delete-grafana": "Delete Grafana",
   "monitors.controls.enable-selected": "Enable selected",
+  "monitors.controls.more-actions": "More monitor operations",
   "monitors.controls.pause-selected": "Pause selected",
   "dashboard.alerts.center-title": "Alert Center",
   "dashboard.alerts.notifications-title": "Alert Notifications",
diff --git a/web-app/src/assets/i18n/zh-CN.json 
b/web-app/src/assets/i18n/zh-CN.json
index 657196dbd5..bda05956b7 100644
--- a/web-app/src/assets/i18n/zh-CN.json
+++ b/web-app/src/assets/i18n/zh-CN.json
@@ -1451,6 +1451,43 @@
   "common.week.5": "星期五",
   "common.week.6": "星期六",
   "common.week.7": "星期日",
+  "time.range.preset": "时间范围",
+  "time.range.relative": "相对时间",
+  "time.range.start": "开始",
+  "time.range.end": "结束",
+  "time.range.from": "开始",
+  "time.range.to": "结束",
+  "time.range.absolute-title": "绝对时间范围",
+  "time.range.quick-ranges": "快捷时间范围",
+  "time.range.recent-ranges": "最近使用",
+  "time.range.custom-range": "自定义范围",
+  "time.range.custom-name": "范围名称",
+  "time.range.save-custom-range": "保存范围",
+  "time.range.delete-custom-range": "删除",
+  "time.range.validation-valid": "时间表达式有效",
+  "time.range.validation-invalid": "时间表达式无效",
+  "time.range.year": "年",
+  "time.range.month": "月",
+  "time.range.date": "日期",
+  "time.range.unset": "未设置",
+  "time.range.hour": "时",
+  "time.range.minute": "分",
+  "time.range.second": "秒",
+  "time.range.previous-month": "上个月",
+  "time.range.next-month": "下个月",
+  "time.range.refresh": "刷新间隔",
+  "time.range.manual-refresh": "手动",
+  "time.range.live": "实时",
+  "time.range.live-on": "暂停实时刷新",
+  "time.range.live-off": "恢复实时刷新",
+  "time.range.timezone": "时区",
+  "time.range.local-timezone": "本地时区",
+  "time.range.apply": "应用",
+  "time.range.apply-aria": "应用时间范围",
+  "time.range.refresh-action": "立即刷新",
+  "time.range.reset": "重置",
+  "time.range.reset-aria": "重置时间范围",
+  "time.range.relative-placeholder": "45m",
   "common.yes": "是",
   "dashboard.alerts.deal": "告警处理",
   "dashboard.alerts.deal-percent": "告警处理率",
@@ -3046,7 +3083,7 @@
   "monitors.loading": "正在从现有监控 API 加载监控列表。",
   "monitors.kicker": "监控工作区",
   "monitors.title": "监控中心",
-  "monitors.subtitle": "监控筛选、结果列表和详情侧栏现在共用同一个工作区外壳。",
+  "monitors.subtitle": "查看传统采集资源、运行状态、标签和基础操作。",
   "monitors.workspace": "监控",
   "monitors.fact.total": "监控总数",
   "monitors.fact.page-count": "当前页",
@@ -3061,9 +3098,15 @@
   "monitors.labels.placeholder": "key=value 或关键词",
   "common.status": "状态",
   "monitors.status.all": "全部状态",
-  "monitors.status.up": "运行中",
-  "monitors.status.down": "异常",
-  "monitors.status.paused": "已暂停",
+  "monitors.status.up": "正常",
+  "monitors.status.down": "宕机",
+  "monitors.status.paused": "暂停",
+  "monitors.type.all": "全部类型",
+  "monitors.app-picker.catalog-title": "类型目录",
+  "monitors.app-picker.close": "关闭",
+  "monitors.app-picker.empty": "没有匹配项",
+  "monitors.app-picker.item-count": "项",
+  "monitors.app-picker.search-placeholder": "搜索可见名称",
   "monitors.section.list.title": "监控列表",
   "monitors.section.list.copy": "结果已整合为一个扁平的工作区列表。",
   "monitors.empty-results.title": "没有匹配的监控",
@@ -3246,6 +3289,8 @@
   "common.none": "无",
   "common.clear-selection": "清除选择",
   "common.select-all": "选择当前页",
+  "common.select-page": "选择当前页",
+  "common.select-all-results": "选择全部",
   "common.import": "导入监控",
   "common.selected": "已选择",
   "common.export-selected-json": "导出所选 JSON",
@@ -3257,12 +3302,15 @@
   "common.page-size": "每页条数",
   "common.previous": "上一条",
   "common.next": "下一条",
+  "common.previous-page": "上一页",
+  "common.next-page": "下一页",
   "common.pause": "暂停",
   "common.delete-success": "删除成功",
   "common.delete-failed": "删除失败",
   "monitor.copy.action": "复制监控",
   "monitor.detail.delete-grafana": "删除 Grafana",
   "monitors.controls.enable-selected": "启用选中项",
+  "monitors.controls.more-actions": "更多监控操作",
   "monitors.controls.pause-selected": "暂停选中项",
   "dashboard.alerts.center-title": "告警中心",
   "dashboard.alerts.notifications-title": "告警通知",
diff --git a/web-next/lib/types.ts b/web-next/lib/types.ts
index aad9d0f81a..cfbb6e2c3f 100644
--- a/web-next/lib/types.ts
+++ b/web-next/lib/types.ts
@@ -138,6 +138,9 @@ export interface Monitor {
   description?: string;
   labels?: Record<string, string>;
   annotations?: Record<string, string>;
+  _displayStatus?: 'ACTIVE' | 'DISAPPEARED';
+  _disappearTime?: number;
+  _graceTimer?: unknown;
   gmtUpdate?: number;
   gmtCreate?: number;
 }
@@ -305,10 +308,23 @@ export interface OtlpBoundEntity {
   monitorBindCount: number;
 }
 
+export interface OtlpUnboundEntityCandidate {
+  suggestedName?: string;
+  suggestedType?: string;
+  namespace?: string;
+  environment?: string;
+  primaryIdentityKey?: string;
+  primaryIdentityValue?: string;
+  signals?: string[];
+  canonicalIdentities?: Record<string, string>;
+  latestObservedAt?: number | null;
+}
+
 export interface OtlpEntityBindingSummary {
   canonicalIdentityKeys: string[];
   recentServices: string[];
   recentBoundEntities: OtlpBoundEntity[];
+  recentUnboundCandidates?: OtlpUnboundEntityCandidate[];
   recentIdentitySamples?: Array<{
     key: string;
     value: string;
@@ -493,12 +509,25 @@ export interface EntityDetailDto {
   monitorSummary?: EntityMonitorSummary;
   logSummary?: EntityLogSummary;
   traceSummary?: EntityTraceSummary;
+  unifiedEvidenceSummary?: EntityUnifiedEvidenceSummary;
   boundMonitors?: Monitor[];
   activeAlerts?: unknown[];
   nextActions?: EntityNextAction[];
   noiseControlSummary?: EntityNoiseControlSummary;
 }
 
+export interface EntityUnifiedEvidenceSummary {
+  activeSignalCount?: number;
+  metricsActive?: boolean;
+  logsActive?: boolean;
+  tracesActive?: boolean;
+  metricEvidenceCount?: number;
+  logEvidenceCount?: number;
+  traceEvidenceCount?: number;
+  latestObservedAt?: number | string | null;
+  activeSignals?: string[];
+}
+
 export interface EntityEvidenceSummary {
   activeAlertCount?: number;
   collectorLastSeenAt?: number | string | null;
@@ -787,6 +816,7 @@ export interface StatusPageIncident {
   components?: StatusPageComponent[];
   contents?: Array<{
     id?: number;
+    incidentId?: number;
     message?: string;
     state?: number;
     timestamp?: number | null;
diff --git a/web-next/scripts/frontend-convergence-contract.test.ts 
b/web-next/scripts/frontend-convergence-contract.test.ts
new file mode 100644
index 0000000000..755b4ce553
--- /dev/null
+++ b/web-next/scripts/frontend-convergence-contract.test.ts
@@ -0,0 +1,133 @@
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
+
+import { describe, expect, it } from 'vitest';
+
+import { buildLegacyFrontendParityAudit, validateLegacyFrontendParityGate } 
from '../lib/legacy-frontend-parity';
+import { cutoverHoldRoutes, placeholderRoutes, routeMatrixPaths } from 
'../lib/nav';
+
+const webNextRoot = resolve(__dirname, '..');
+const repoRoot = resolve(webNextRoot, '..');
+
+function readWebNext(path: string): string {
+  return readFileSync(resolve(webNextRoot, path), 'utf8');
+}
+
+function readRepo(path: string): string {
+  return readFileSync(resolve(repoRoot, path), 'utf8');
+}
+
+function readJson(path: string): Record<string, unknown> {
+  return JSON.parse(readRepo(path));
+}
+
+describe('M10 frontend convergence contract', () => {
+  it('keeps the final route cutover free of primary hold or placeholder 
blockers', () => {
+    const audit = buildLegacyFrontendParityAudit();
+    const gate = validateLegacyFrontendParityGate(audit);
+
+    expect(cutoverHoldRoutes.map(route => route.href)).toEqual([]);
+    expect(placeholderRoutes.map(route => route.href)).toEqual([]);
+    expect(audit.releaseBlocked).toBe(false);
+    expect(gate).toEqual({ valid: true, issues: [] });
+  });
+
+  it('keeps the release route matrix covering the M10 action and incident 
workbenches', () => {
+    const routeMatrixScript = readWebNext('scripts/route-matrix.mjs');
+
+    expect(routeMatrixPaths).toEqual(expect.arrayContaining(['/actions', 
'/incidents', '/log/manage', '/trace/manage']));
+    expect(routeMatrixScript).toContain("'/actions'");
+    expect(routeMatrixScript).toContain("'/incidents'");
+    expect(routeMatrixScript).toContain("'/passport/login'");
+  });
+
+  it('keeps core workbench loading and first-screen cache behavior 
centralized', () => {
+    const appFrameSource = readWebNext('components/shell/app-frame.tsx');
+    const clientWorkbenchSource = 
readWebNext('components/workbench/client-workbench.tsx');
+    const cacheSource = readWebNext('lib/workbench-load-cache.ts');
+
+    expect(cacheSource).toContain('settledTtlMs');
+    expect(appFrameSource).toContain("import { consumeWorkbenchLoad } from 
'@/lib/workbench-load-cache'");
+    expect(appFrameSource).toContain('APP_FRAME_HEADER_STATE_CACHE_TTL_MS = 
60_000');
+    expect(appFrameSource).toContain('app-frame:header-state:${locale}');
+    
expect(appFrameSource).not.toContain('app-frame:header-state:${locale}:${pathname}');
+    expect(clientWorkbenchSource).toContain('cacheSettledTtlMs?: number');
+    expect(clientWorkbenchSource).toContain('consumeWorkbenchLoad(cacheKey, 
load, { settledTtlMs: cacheSettledTtlMs })');
+  });
+
+  it('keeps the migrated M10 workbenches on UI Lab backed shared components', 
() => {
+    const uiSource = readWebNext('packages/hertzbeat-ui/src/index.tsx');
+    const uiLabSource = readWebNext('app/ui-lab/page.tsx');
+    const checks = [
+      {
+        route: readWebNext('app/actions/actions-page.tsx'),
+        sharedComponent: 'HzActionWorkbench',
+        uiLabMarker: 'data-hz-ui-lab-action-workbench="shared"',
+        routeMarker: 'data-actions-shared-workbench="hertzbeat-ui"',
+        forbidden: ['OpsSurfacePage', 'angular-dark-ops-placeholder']
+      },
+      {
+        route: readWebNext('app/incidents/incidents-page.tsx'),
+        sharedComponent: 'HzIncidentWorkbench',
+        uiLabMarker: 'data-hz-ui-lab-incident-workbench="shared"',
+        routeMarker: 'data-incidents-shared-workbench="hertzbeat-ui"',
+        forbidden: ['OpsSurfacePage', 'angular-dark-ops-placeholder']
+      },
+      {
+        route: readWebNext('app/explorer/explorer-page.tsx'),
+        sharedComponent: 'HzExplorerFrame',
+        uiLabMarker: '@hertzbeat/ui explorer',
+        routeMarker: 'data-explorer-shared-frame="hertzbeat-ui"',
+        forbidden: ['OpsSurfacePage', 'buildExplorerSurfaceConfig']
+      },
+      {
+        route: readWebNext('app/topology/topology-page.tsx'),
+        sharedComponent: 'HzTopologyWorkbenchFrame',
+        uiLabMarker: 'data-hz-ui-lab-topology-workbench-frame="shared"',
+        routeMarker: 
'data-topology-workbench-frame-owner="hertzbeat-ui-workbench-frame"',
+        forbidden: ['data-topology-static-seed', 
'checkout-api/orders-db/redis']
+      }
+    ];
+
+    checks.forEach(check => {
+      expect(uiSource).toContain(`export function ${check.sharedComponent}`);
+      expect(uiLabSource).toContain(check.uiLabMarker);
+      expect(check.route).toContain(check.sharedComponent);
+      expect(check.route).toContain(check.routeMarker);
+      check.forbidden.forEach(forbidden => {
+        expect(check.route).not.toContain(forbidden);
+      });
+    });
+  });
+
+  it('keeps shared time-range and monitor operator copy available in both 
legacy catalogs', () => {
+    const enMessages = readJson('web-app/src/assets/i18n/en-US.json');
+    const zhMessages = readJson('web-app/src/assets/i18n/zh-CN.json');
+
+    [
+      'time.range.preset',
+      'time.range.apply',
+      'time.range.refresh-action',
+      'monitors.app-picker.catalog-title',
+      'monitors.app-picker.search-placeholder',
+      'monitors.controls.more-actions',
+      'common.select-page',
+      'common.select-all-results'
+    ].forEach(key => {
+      expect(enMessages[key], `${key} en-US`).toBeTruthy();
+      expect(zhMessages[key], `${key} zh-CN`).toBeTruthy();
+    });
+  });
+
+  it('keeps frontend DTOs aligned with the final M10 entity, OTLP, monitor, 
and status evidence surfaces', () => {
+    const typesSource = readWebNext('lib/types.ts');
+
+    expect(typesSource).toContain("_displayStatus?: 'ACTIVE' | 'DISAPPEARED'");
+    expect(typesSource).toContain('_disappearTime?: number');
+    expect(typesSource).toContain('export interface 
OtlpUnboundEntityCandidate');
+    expect(typesSource).toContain('recentUnboundCandidates?: 
OtlpUnboundEntityCandidate[]');
+    expect(typesSource).toContain('unifiedEvidenceSummary?: 
EntityUnifiedEvidenceSummary');
+    expect(typesSource).toContain('export interface 
EntityUnifiedEvidenceSummary');
+    expect(typesSource).toContain('incidentId?: number');
+  });
+});


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

Reply via email to